[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n\n# Matches multiple files with brace expansion notation\n# Set default charset\n[*.*]\ncharset = utf-8\nindent_style = tab\nindent_size = 4\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"node-rtmpapi\"]\n\tpath = node-rtmpapi\n\turl = https://github.com/yingDev/node-rtmpapi.git\n[submodule \"websockify\"]\n\tpath = websockify\n\turl = https://github.com/yingDev/websockify.git\n"
  },
  {
    "path": ".idea/encodings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Encoding\">\n    <file url=\"PROJECT\" charset=\"UTF-8\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"JavaScriptSettings\">\n    <option name=\"languageLevel\" value=\"ES6\" />\n  </component>\n  <component name=\"ProjectLevelVcsManager\" settingsEditedManually=\"false\">\n    <OptionsSetting value=\"true\" id=\"Add\" />\n    <OptionsSetting value=\"true\" id=\"Remove\" />\n    <OptionsSetting value=\"true\" id=\"Checkout\" />\n    <OptionsSetting value=\"true\" id=\"Update\" />\n    <OptionsSetting value=\"true\" id=\"Status\" />\n    <OptionsSetting value=\"true\" id=\"Edit\" />\n    <ConfirmationsSetting value=\"0\" id=\"Add\" />\n    <ConfirmationsSetting value=\"0\" id=\"Remove\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/web-rtmp.iml\" filepath=\"$PROJECT_DIR$/.idea/web-rtmp.iml\" />\n    </modules>\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/node-rtmpapi\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/web-rtmp.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\">\n    <content url=\"file://$MODULE_DIR$\">\n      <excludeFolder url=\"file://$MODULE_DIR$/build\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "README.md",
    "content": "# Web-Rtmp\n在网页上播放RTMP视频流，通过Websocket。\n\n# WARNING ============\n这个项目很大程度上只能算可行性验证，不适合作为 library 使用。试验结论是：用 js 解码视频效率已经够低了，rtmp 协议增加了许多额外开销，如果单纯为了播放视频，并不明智。不如用纯粹的 ws 传送 h264 流。\n\n## 基本原理\n- 服务端\n\t- 使用 [websockify](https://github.com/kanaka/websockify)  wrap 一个 rtmp 服务器地址。 ([yingDev的fork](https://github.com/yingDev/websockify) 去掉了base64子协议检查)\n\t\n     ```bash\n     ./websockify.py 1999 <rtmp_server>:1935\n     ```\n- 浏览器\n\t- 使用 [node-rtmpapi](https://github.com/delian/node-rtmpapi) 解析 RTMP 协议，完成握手和通信。 ([yingDev的fork](https://github.com/yingDev/node-rtmpapi) 增加了浏览器支持、修正了几个错误)\n\n\t- 提取其中的 H264 视频流\n\n\t- 喂给 [Broadway](https://github.com/mbebenita/Broadway) 解码\n\t\n   \t ```js\n    decoder.decode(frame);\n   \t ```\n    \n## 使用\n```js\n//比如 rtmp://helloworld.com/live/abc ---> app='live', streamName='abc', rtmp_server='helloworld.com'\n// ./websockify.py 1999 helloworld.com:1935\nvar player = new WebRtmpPlayer('ws://127.0.0.1:1999', '<app>', '<streamName>', 'rtmp://<rtmp_server>/<app>');\nplayer.canvas.style['height'] = '100%';\ndocument.getElementById(\"vidCont\").appendChild(player.canvas);\n```\n    \n## 运行\n```bash\ngit clone https://github.com/yingDev/Web-Rtmp.git\ncd Web-Rtmp\ngit submodule update --init --recursive\ncnpm install\n```\n\n```bash\n# set your rtmp params in test.js first, then \nwebpack -w\n```\n```bash\n# setup test server\n./websockify/websockify.py 1999 <rtmp_server>:1935\n```\n```bash\nopen index.html\n```\n\n## 局限\n- Broadway: \n   <blockquote> The decoder ...does not support weighted prediction for P-frames and CABAC entropy encoding...</blockquote>\n\n \n## 参考资料\n- Real-Time Messaging Protocol (RTMP) specification <br>\nhttp://www.adobe.com/devnet/rtmp.html\n\n- FLV and F4V File Format Specification <br>\nhttp://www.adobe.com/devnet/f4v.html\n\n- h264-live-player <br> https://github.com/131/h264-live-player\n"
  },
  {
    "path": "WebRtmpPlayer.js",
    "content": "var RTMP = require('./node-rtmpapi');\nvar SimpleWebsocket = require('simple-websocket');\nvar Buffer = require('buffer').Buffer;\n\nconst H264_SEP = new Buffer([0,0,0,1]);\nconst FRAME_Q_SIZE = 15;\n\nclass WebRtmpPlayer\n{\n\tconstructor(wsHost, app, streamName, tcUrl)\n\t{\n\t\tthis._frameQ = [];\n\t\tthis._fps = NaN;\n\t\tthis._lastRenderTime = 0;\n\n\t\tthis._decoder = new Decoder();\n\t\tthis._player = new Player({ useWorker: false });\n\t\tthis._url = {host: wsHost, app: app, tcUrl: tcUrl, stream: streamName};\n\n\t\tthis._decoder.onPictureDecoded = this._onPictureDecoded.bind(this);\n\n\t\tthis._rtmpTransId = 0;\n\t\tthis._invokeChannel = null;\n\t\tthis._videoChannel = null;\n\t\tthis._rtmpSession = null;\n\n\t\tthis._sock = new SimpleWebsocket(this._url.host);\n\t\tthis._sock.setMaxListeners(100);\n\t\tthis._sock.on('connect', ()=>\n\t\t{\n\t\t\tnew RTMP.rtmpSession(this._sock, true, this._onRtmpSessionCreated.bind(this));\n\t\t})\n\t}\n\n\tget canvas() { return this._player.canvas; }\n\n\t_onPictureDecoded(buffer, width, height, infos)\n\t{\n\t\tif(this._frameQ.length === FRAME_Q_SIZE)\n\t\t{\n\t\t\tconsole.log(\"** drop oldest frame!\");\n\t\t\tthis._frameQ.shift(); //如果播放速度跟不上，扔掉最老那一帧\n\t\t}\n\t\tthis._frameQ.push({data: Buffer.from(buffer), width: width, height: height, canvasObj: this._player.canvasObj});\n\t}\n\n\t_drawFrame()\n\t{\n\t\tvar now = new Date();//如果播放速度跟不上网络速度，跳帧\n\t\tvar skipFrame = Math.floor(Math.abs(now - this._lastRenderTime) / (1000 / this._fps) ) - 1;\n\t\tif(this._lastRenderTime && skipFrame > 0)\n\t\t{\n\t\t\tconsole.log(\"SkipFrmae = \" + skipFrame);\n\t\t\t//while(skipFrame-- > 0 && this._frameQ.length > 0) this._frameQ.shift();\n\t\t}\n\n\t\tvar frame = this._frameQ.shift();\n\t\tif(frame)\n\t\t{\n\t\t\tthis._player.renderFrame(frame);\n\t\t}\n\t\tthis._lastRenderTime = now;\n\t}\n\n\t_rtmpConnect()\n\t{\n\t\tthis._rtmpSession.Q.Q(0,() =>\n\t\t{\n\t\t\tconsole.log(\"sending connect\");\n\n\t\t\tthis._invokeChannel.sendAmf0EncCmdMsg({\n\t\t\t\tcmd: 'connect',\n\t\t\t\ttransId:++this._rtmpTransId,\n\t\t\t\tcmdObj:\n\t\t\t\t{\n\t\t\t\t\tapp: this._url.app,\n\t\t\t\t\ttcUrl: this._url.tcUrl,\n\t\t\t\t\tfpad: false,\n\t\t\t\t\tcapabilities: 15.0, //note: 我不知道这些参数什么鬼，依据rtmpdump分析出来的\n\t\t\t\t\taudioCodecs: 3191,\n\t\t\t\t\tvideoCodecs: 252,\n\t\t\t\t\tvideoFunction: 1.0\n\t\t\t\t}\n\t\t\t});\n\t\t\tthis._invokeChannel.invokedMethods[this._rtmpTransId] = 'connect';\n\t\t});\n\t}\n\n\t_rtmpCreateStream()\n\t{\n\t\tthis._rtmpSession.Q.Q(0, ()=>\n\t\t{\n\t\t\tconsole.log(\"sending createStream\");\n\t\t\tthis._invokeChannel.sendAmf0EncCmdMsg({\n\t\t\t\tcmd: 'createStream',\n\t\t\t\ttransId: ++this._rtmpTransId,\n\t\t\t\tcmdObj: null\n\t\t\t});\n\t\t\tthis._invokeChannel.invokedMethods[this._rtmpTransId] = 'createStream';\n\t\t});\n\t}\n\n\t_rtmpSendPlay(msgStreamId)\n\t{\n\t\tthis._rtmpSession.Q.Q(0, ()=>\n\t\t{\n\t\t\tthis._videoChannel.chunk.msgStreamId = msgStreamId;\n\t\t\t//send play ??\n\t\t\tthis._videoChannel.sendAmf0EncCmdMsg({\n\t\t\t\tcmd: 'play',\n\t\t\t\ttransId: ++this._rtmpTransId,\n\t\t\t\tcmdObj:null,\n\t\t\t\tstreamName: this._url.stream,\n\t\t\t\tstart:-2\n\n\t\t\t},0);\n\t\t\tthis._invokeChannel.invokedMethods[this._rtmpTransId] = \"play\";\n\t\t});\n\t}\n\n\t_onRtmpSessionCreated(session)\n\t{\n\t\tthis._rtmpSession = session;\n\t\tconsole.log(\"rtmpSession...cb...\");\n\t\tthis._invokeChannel = new RTMP.rtmpChunk.RtmpChunkMsgClass({streamId:5}, {sock: this._sock, Q: session.Q, debug: false});\n\t\tthis._invokeChannel.invokedMethods = {}; //用来保存invoke的次数，以便收到消息的时候确认对应结果\n\t\tthis._videoChannel = new RTMP.rtmpChunk.RtmpChunkMsgClass({streamId:8}, {sock: this._sock, Q: session.Q, debug: false});\n\n\t\tsession.Q.Q(0,this._rtmpConnect.bind(this));\n\t\tsession.Q.Q(0, () =>\n\t\t{\n\t\t\tconsole.log(\"Begin LOOP\");\n\t\t\tsession.msg.loop(this._handleRtmpMessage.bind(this));\n\t\t});\n\t}\n\n\t _handleRtmpMessage(chunkMsg)\n\t{\n\t\tvar chunk = chunkMsg.chunk;\n\t\tvar msg = chunk.msg;\n\n\t\tconsole.log(\"GOT MESSAGE: \" + chunk.msgTypeText);\n\t\t//console.log(\"===========>\\n\" + JSON.stringify(msg));\n\n\t\tif(chunk.msgTypeText == \"amf0cmd\")\n\t\t{\n\t\t\tif(msg.cmd == \"_result\")\n\t\t\t{\n\t\t\t\tvar lastInvoke = this._invokeChannel.invokedMethods[msg.transId];\n\t\t\t\tif(lastInvoke)\n\t\t\t\t{\n\t\t\t\t\tconsole.log(\"<--Got Invoke Result for: \" + lastInvoke);\n\t\t\t\t\tdelete this._invokeChannel.invokedMethods[msg.transId];\n\t\t\t\t}\n\n\t\t\t\tswitch (lastInvoke)\n\t\t\t\t{\n\t\t\t\t\tcase 'connect':\n\t\t\t\t\t\treturn this._rtmpCreateStream();\n\t\t\t\t\tcase 'createStream':\n\t\t\t\t\t\treturn this._rtmpSendPlay(msg.info);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif(chunk.msgTypeText == \"video\")\n\t\t{\n\t\t\t//提取h264流\n\t\t\tvar chunkData = chunk.data;\n\t\t\tif (chunkData.length > 4)\n\t\t\t{\n\t\t\t\tif (chunkData[1] === 1)\n\t\t\t\t{\n\t\t\t\t\tchunkData = Buffer.concat([H264_SEP, chunkData.slice(9)]);\n\t\t\t\t}\n\t\t\t\telse if (chunkData[1] === 0)\n\t\t\t\t{\n\t\t\t\t\tvar spsSize = (chunkData[11] << 8) | chunkData[12];\n\t\t\t\t\tvar spsEnd = 13 + spsSize;\n\t\t\t\t\tchunkData = Buffer.concat([H264_SEP, chunkData.slice(13, spsEnd), H264_SEP, chunkData.slice(spsEnd + 3)]);\n\t\t\t\t}\n\t\t\t\tthis._decoder.decode(chunkData);\n\t\t\t}\n\t\t}\n\n\t\tif(chunk.msgTypeText == \"amf0meta\" && msg.cmd == 'onMetaData')\n\t\t{\n\t\t\tconsole.log(\"onmetadata\");\n\t\t\tthis._fps = chunk.msg['event']['framerate'];\n\t\t\tconsole.log(\"fps = \"+this._fps);\n\t\t\tsetInterval(this._drawFrame.bind(this), 1000.0/this._fps); //todo: clear\n\t\t}\n\n\t\tthis._rtmpSession.Q.Q(0,()=>\n\t\t{\n\t\t\tthis._rtmpSession.msg.loop(this._handleRtmpMessage.bind(this));\n\t\t});\n\t}\n}\n\nmodule.exports = WebRtmpPlayer;"
  },
  {
    "path": "index.template.html",
    "content": "<html>\n<head>\n<title>rtmp test</title>\n    <script src=\"./node_modules/broadway-player/Player/Decoder.js\"></script>\n    <script src=\"./node_modules/broadway-player/Player/YUVCanvas.js\"></script>\n    <script src=\"./node_modules/broadway-player/Player/Player.js\"></script>\n    <script src=\"./node_modules/broadway-player/Player/stream.js\"></script>\n    <script src=\"./node_modules/broadway-player/Player/Player.js\"></script>\n</head>\n\n<body>\n\tbuild time: {{buildTime}}\n\n\t<div id=\"vidCont\" style=\"width:640px; height:360px;\">\n\t</div>\n\n\t<script type=\"text/javascript\" src=\"build/bundle.js\"></script>\n\t<script type=\"text/javascript\">\n\t\t\n\t</script>\n</body>\n\n</html>\n"
  },
  {
    "path": "misc/do-rtmpsuck.sh",
    "content": "#!/bin/bash\n\n# enable internal port forwarding:\nsudo sysctl -w net.inet.ip.forwarding=1\n\n# apply the pf rules:\necho '\nrdr pass log on lo0 proto tcp from en0 to any port 1935 -> 127.0.0.1\npass out on en0 route-to lo0 inet proto tcp from en0 to any port 1935 keep state user != root\n' | sudo pfctl -ef -\n\n# check the pf rules:\n# sudo pfctl -s all\nsay \"starting r-t-m-p-suck\";\n\nsudo rtmpsuck $@;\n\nsay \"r-t-m-p-suck Stopped.\";\n\n# clear the pf rules:\necho ''\necho ''\necho \"====== restting pfctl rules =======\"\necho ''\n\nsudo pfctl -F all -f /etc/pf.conf"
  },
  {
    "path": "misc/reloadChrome.scpt",
    "content": "on run {targetUrl}\n    tell application \"Google Chrome\"\n        activate\n\n        set theUrl to my remove_http(targetUrl)\n\n        if (count every window) = 0 then\n            make new window\n        end if\n\n        set found to false\n        set theTabIndex to -1\n        repeat with theWindow in every window\n            set theTabIndex to 0\n\n            repeat with theTab in every tab of theWindow\n                set theTabIndex to theTabIndex + 1\n                set theTabUrl to my remove_http(theTab's URL as string)\n\n                if (theTabUrl contains theUrl) then\n                    set found to true\n                    exit repeat\n                end if\n\n            end repeat\n\n            if found then\n                exit repeat\n            end if\n        end repeat\n\n        if found then\n            tell theTab to reload\n            set theWindow's active tab index to theTabIndex\n            set index of theWindow to 1\n        else\n            tell window 1 to make new tab with properties {URL:targetUrl}\n        end if\n    end tell\nend run\n\non remove_http(input_url)\n    if (input_url contains \"https://\") then\n         return trim_line(input_url, \"https://\")\n    else\n         return trim_line(input_url, \"http://\")\n    end if\n    return input_url\nend remove_http\n\n-- Taken from: http://www.macosxautomation.com/applescript/sbrt/sbrt-06.html --\non trim_line(this_text, trim_chars)\n    set x to the length of the trim_chars\n    -- TRIM BEGINNING\n    repeat while this_text begins with the trim_chars\n        try\n            set this_text to characters (x + 1) thru -1 of this_text as string\n        on error\n            -- the text contains nothing but the trim characters\n            return \"\"\n        end try\n    end repeat\n    return this_text\nend trim_line"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"web-rtmp\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Play rtmp video stream on the web page\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"yingDev\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"babel-preset-es2015\": \"^6.16.0\",\n    \"broadway-player\": \"^0.1.1\",\n    \"buffer\": \"^5.0.0\",\n    \"simple-websocket\": \"^4.1.0\"\n  },\n  \"devDependencies\": {\n    \"babel-core\": \"^6.17.0\",\n    \"babel-loader\": \"^6.2.5\",\n    \"babel-preset-es2015\": \"^6.16.0\",\n    \"webpack\": \"^1.13.2\",\n    \"webpack-shell-plugin\": \"^0.4.3\",\n    \"websockify\": \"^0.7.1\"\n  }\n}\n"
  },
  {
    "path": "test.js",
    "content": "const WebRtmpPlayer = require('./WebRtmpPlayer');\n\n//note: tcUrl是原始rtmp地址，不含流名称。参见rtmp spec\nalert('set your rtmp params in test.js first!');\nvar player = new WebRtmpPlayer('ws://127.0.0.1:1999', '<app>', '<streamName>', 'rtmp://<rtmp_server>/<app>');\nplayer.canvas.style['height'] = '100%';\ndocument.getElementById(\"vidCont\").appendChild(player.canvas);\n"
  },
  {
    "path": "webpack.config.js",
    "content": "var webpack = require('webpack');\nconst WebpackShellPlugin = require('webpack-shell-plugin');\n\nmodule.exports = {\n    entry: \"./test.js\",\n    output: {\n        path: __dirname + \"/build/\",\n        filename: \"bundle.js\"\n    },\n    devtool: \"source-map\",\n    module: {\n        loaders: [\n            {\n                test: /\\.js$/,\n                exclude: /(node_modules, build)/,\n                loader: 'babel',\n\t            query: {\n\t\t            presets: ['es2015']\n\t            }\n            }\n        ]\n    },\n\texternals: {\n\t\t\"broadway\": \"broadway-player\"\n\t},\n\tplugins:[\n\t\tnew webpack.optimize.DedupePlugin(),\n\t\tnew WebpackShellPlugin({\n\t\t\tonBuildStart:[/*'say begin'*/],\n\t\t\tonBuildEnd:[\n\t\t\t\t'sed \"s/{{buildTime}}/$(date)/g\" index.template.html > index.html',\n\t\t\t\t//'say end!'\n\t\t\t\t//'say world; open \"/Applications/Google Chrome.app\"',\n\t\t\t\t//'sleep 1; say chrome & osascript ./misc/reloadChrome.scpt \"http://localhost:63342/web-rtmp/index.html\"'\n\t\t\t],\n\t\t\tdev:false}\n\t\t)\n\t]\n};\n"
  }
]