Repository: yingDev/Web-Rtmp Branch: master Commit: bc85b5c7e03d Files: 16 Total size: 13.6 KB Directory structure: gitextract_ok_mgr1a/ ├── .editorconfig ├── .gitignore ├── .gitmodules ├── .idea/ │ ├── encodings.xml │ ├── misc.xml │ ├── modules.xml │ ├── vcs.xml │ └── web-rtmp.iml ├── README.md ├── WebRtmpPlayer.js ├── index.template.html ├── misc/ │ ├── do-rtmpsuck.sh │ └── reloadChrome.scpt ├── package.json ├── test.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # Matches multiple files with brace expansion notation # Set default charset [*.*] charset = utf-8 indent_style = tab indent_size = 4 trim_trailing_whitespace = true ================================================ FILE: .gitignore ================================================ node_modules ================================================ FILE: .gitmodules ================================================ [submodule "node-rtmpapi"] path = node-rtmpapi url = https://github.com/yingDev/node-rtmpapi.git [submodule "websockify"] path = websockify url = https://github.com/yingDev/websockify.git ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: .idea/web-rtmp.iml ================================================ ================================================ FILE: README.md ================================================ # Web-Rtmp 在网页上播放RTMP视频流,通过Websocket。 # WARNING ============ 这个项目很大程度上只能算可行性验证,不适合作为 library 使用。试验结论是:用 js 解码视频效率已经够低了,rtmp 协议增加了许多额外开销,如果单纯为了播放视频,并不明智。不如用纯粹的 ws 传送 h264 流。 ## 基本原理 - 服务端 - 使用 [websockify](https://github.com/kanaka/websockify) wrap 一个 rtmp 服务器地址。 ([yingDev的fork](https://github.com/yingDev/websockify) 去掉了base64子协议检查) ```bash ./websockify.py 1999 :1935 ``` - 浏览器 - 使用 [node-rtmpapi](https://github.com/delian/node-rtmpapi) 解析 RTMP 协议,完成握手和通信。 ([yingDev的fork](https://github.com/yingDev/node-rtmpapi) 增加了浏览器支持、修正了几个错误) - 提取其中的 H264 视频流 - 喂给 [Broadway](https://github.com/mbebenita/Broadway) 解码 ```js decoder.decode(frame); ``` ## 使用 ```js //比如 rtmp://helloworld.com/live/abc ---> app='live', streamName='abc', rtmp_server='helloworld.com' // ./websockify.py 1999 helloworld.com:1935 var player = new WebRtmpPlayer('ws://127.0.0.1:1999', '', '', 'rtmp:///'); player.canvas.style['height'] = '100%'; document.getElementById("vidCont").appendChild(player.canvas); ``` ## 运行 ```bash git clone https://github.com/yingDev/Web-Rtmp.git cd Web-Rtmp git submodule update --init --recursive cnpm install ``` ```bash # set your rtmp params in test.js first, then webpack -w ``` ```bash # setup test server ./websockify/websockify.py 1999 :1935 ``` ```bash open index.html ``` ## 局限 - Broadway:
The decoder ...does not support weighted prediction for P-frames and CABAC entropy encoding...
## 参考资料 - Real-Time Messaging Protocol (RTMP) specification
http://www.adobe.com/devnet/rtmp.html - FLV and F4V File Format Specification
http://www.adobe.com/devnet/f4v.html - h264-live-player
https://github.com/131/h264-live-player ================================================ FILE: WebRtmpPlayer.js ================================================ var RTMP = require('./node-rtmpapi'); var SimpleWebsocket = require('simple-websocket'); var Buffer = require('buffer').Buffer; const H264_SEP = new Buffer([0,0,0,1]); const FRAME_Q_SIZE = 15; class WebRtmpPlayer { constructor(wsHost, app, streamName, tcUrl) { this._frameQ = []; this._fps = NaN; this._lastRenderTime = 0; this._decoder = new Decoder(); this._player = new Player({ useWorker: false }); this._url = {host: wsHost, app: app, tcUrl: tcUrl, stream: streamName}; this._decoder.onPictureDecoded = this._onPictureDecoded.bind(this); this._rtmpTransId = 0; this._invokeChannel = null; this._videoChannel = null; this._rtmpSession = null; this._sock = new SimpleWebsocket(this._url.host); this._sock.setMaxListeners(100); this._sock.on('connect', ()=> { new RTMP.rtmpSession(this._sock, true, this._onRtmpSessionCreated.bind(this)); }) } get canvas() { return this._player.canvas; } _onPictureDecoded(buffer, width, height, infos) { if(this._frameQ.length === FRAME_Q_SIZE) { console.log("** drop oldest frame!"); this._frameQ.shift(); //如果播放速度跟不上,扔掉最老那一帧 } this._frameQ.push({data: Buffer.from(buffer), width: width, height: height, canvasObj: this._player.canvasObj}); } _drawFrame() { var now = new Date();//如果播放速度跟不上网络速度,跳帧 var skipFrame = Math.floor(Math.abs(now - this._lastRenderTime) / (1000 / this._fps) ) - 1; if(this._lastRenderTime && skipFrame > 0) { console.log("SkipFrmae = " + skipFrame); //while(skipFrame-- > 0 && this._frameQ.length > 0) this._frameQ.shift(); } var frame = this._frameQ.shift(); if(frame) { this._player.renderFrame(frame); } this._lastRenderTime = now; } _rtmpConnect() { this._rtmpSession.Q.Q(0,() => { console.log("sending connect"); this._invokeChannel.sendAmf0EncCmdMsg({ cmd: 'connect', transId:++this._rtmpTransId, cmdObj: { app: this._url.app, tcUrl: this._url.tcUrl, fpad: false, capabilities: 15.0, //note: 我不知道这些参数什么鬼,依据rtmpdump分析出来的 audioCodecs: 3191, videoCodecs: 252, videoFunction: 1.0 } }); this._invokeChannel.invokedMethods[this._rtmpTransId] = 'connect'; }); } _rtmpCreateStream() { this._rtmpSession.Q.Q(0, ()=> { console.log("sending createStream"); this._invokeChannel.sendAmf0EncCmdMsg({ cmd: 'createStream', transId: ++this._rtmpTransId, cmdObj: null }); this._invokeChannel.invokedMethods[this._rtmpTransId] = 'createStream'; }); } _rtmpSendPlay(msgStreamId) { this._rtmpSession.Q.Q(0, ()=> { this._videoChannel.chunk.msgStreamId = msgStreamId; //send play ?? this._videoChannel.sendAmf0EncCmdMsg({ cmd: 'play', transId: ++this._rtmpTransId, cmdObj:null, streamName: this._url.stream, start:-2 },0); this._invokeChannel.invokedMethods[this._rtmpTransId] = "play"; }); } _onRtmpSessionCreated(session) { this._rtmpSession = session; console.log("rtmpSession...cb..."); this._invokeChannel = new RTMP.rtmpChunk.RtmpChunkMsgClass({streamId:5}, {sock: this._sock, Q: session.Q, debug: false}); this._invokeChannel.invokedMethods = {}; //用来保存invoke的次数,以便收到消息的时候确认对应结果 this._videoChannel = new RTMP.rtmpChunk.RtmpChunkMsgClass({streamId:8}, {sock: this._sock, Q: session.Q, debug: false}); session.Q.Q(0,this._rtmpConnect.bind(this)); session.Q.Q(0, () => { console.log("Begin LOOP"); session.msg.loop(this._handleRtmpMessage.bind(this)); }); } _handleRtmpMessage(chunkMsg) { var chunk = chunkMsg.chunk; var msg = chunk.msg; console.log("GOT MESSAGE: " + chunk.msgTypeText); //console.log("===========>\n" + JSON.stringify(msg)); if(chunk.msgTypeText == "amf0cmd") { if(msg.cmd == "_result") { var lastInvoke = this._invokeChannel.invokedMethods[msg.transId]; if(lastInvoke) { console.log("<--Got Invoke Result for: " + lastInvoke); delete this._invokeChannel.invokedMethods[msg.transId]; } switch (lastInvoke) { case 'connect': return this._rtmpCreateStream(); case 'createStream': return this._rtmpSendPlay(msg.info); } } } if(chunk.msgTypeText == "video") { //提取h264流 var chunkData = chunk.data; if (chunkData.length > 4) { if (chunkData[1] === 1) { chunkData = Buffer.concat([H264_SEP, chunkData.slice(9)]); } else if (chunkData[1] === 0) { var spsSize = (chunkData[11] << 8) | chunkData[12]; var spsEnd = 13 + spsSize; chunkData = Buffer.concat([H264_SEP, chunkData.slice(13, spsEnd), H264_SEP, chunkData.slice(spsEnd + 3)]); } this._decoder.decode(chunkData); } } if(chunk.msgTypeText == "amf0meta" && msg.cmd == 'onMetaData') { console.log("onmetadata"); this._fps = chunk.msg['event']['framerate']; console.log("fps = "+this._fps); setInterval(this._drawFrame.bind(this), 1000.0/this._fps); //todo: clear } this._rtmpSession.Q.Q(0,()=> { this._rtmpSession.msg.loop(this._handleRtmpMessage.bind(this)); }); } } module.exports = WebRtmpPlayer; ================================================ FILE: index.template.html ================================================ rtmp test build time: {{buildTime}}
================================================ FILE: misc/do-rtmpsuck.sh ================================================ #!/bin/bash # enable internal port forwarding: sudo sysctl -w net.inet.ip.forwarding=1 # apply the pf rules: echo ' rdr pass log on lo0 proto tcp from en0 to any port 1935 -> 127.0.0.1 pass out on en0 route-to lo0 inet proto tcp from en0 to any port 1935 keep state user != root ' | sudo pfctl -ef - # check the pf rules: # sudo pfctl -s all say "starting r-t-m-p-suck"; sudo rtmpsuck $@; say "r-t-m-p-suck Stopped."; # clear the pf rules: echo '' echo '' echo "====== restting pfctl rules =======" echo '' sudo pfctl -F all -f /etc/pf.conf ================================================ FILE: misc/reloadChrome.scpt ================================================ on run {targetUrl} tell application "Google Chrome" activate set theUrl to my remove_http(targetUrl) if (count every window) = 0 then make new window end if set found to false set theTabIndex to -1 repeat with theWindow in every window set theTabIndex to 0 repeat with theTab in every tab of theWindow set theTabIndex to theTabIndex + 1 set theTabUrl to my remove_http(theTab's URL as string) if (theTabUrl contains theUrl) then set found to true exit repeat end if end repeat if found then exit repeat end if end repeat if found then tell theTab to reload set theWindow's active tab index to theTabIndex set index of theWindow to 1 else tell window 1 to make new tab with properties {URL:targetUrl} end if end tell end run on remove_http(input_url) if (input_url contains "https://") then return trim_line(input_url, "https://") else return trim_line(input_url, "http://") end if return input_url end remove_http -- Taken from: http://www.macosxautomation.com/applescript/sbrt/sbrt-06.html -- on trim_line(this_text, trim_chars) set x to the length of the trim_chars -- TRIM BEGINNING repeat while this_text begins with the trim_chars try set this_text to characters (x + 1) thru -1 of this_text as string on error -- the text contains nothing but the trim characters return "" end try end repeat return this_text end trim_line ================================================ FILE: package.json ================================================ { "name": "web-rtmp", "version": "1.0.0", "description": "Play rtmp video stream on the web page", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "yingDev", "license": "ISC", "dependencies": { "babel-preset-es2015": "^6.16.0", "broadway-player": "^0.1.1", "buffer": "^5.0.0", "simple-websocket": "^4.1.0" }, "devDependencies": { "babel-core": "^6.17.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.16.0", "webpack": "^1.13.2", "webpack-shell-plugin": "^0.4.3", "websockify": "^0.7.1" } } ================================================ FILE: test.js ================================================ const WebRtmpPlayer = require('./WebRtmpPlayer'); //note: tcUrl是原始rtmp地址,不含流名称。参见rtmp spec alert('set your rtmp params in test.js first!'); var player = new WebRtmpPlayer('ws://127.0.0.1:1999', '', '', 'rtmp:///'); player.canvas.style['height'] = '100%'; document.getElementById("vidCont").appendChild(player.canvas); ================================================ FILE: webpack.config.js ================================================ var webpack = require('webpack'); const WebpackShellPlugin = require('webpack-shell-plugin'); module.exports = { entry: "./test.js", output: { path: __dirname + "/build/", filename: "bundle.js" }, devtool: "source-map", module: { loaders: [ { test: /\.js$/, exclude: /(node_modules, build)/, loader: 'babel', query: { presets: ['es2015'] } } ] }, externals: { "broadway": "broadway-player" }, plugins:[ new webpack.optimize.DedupePlugin(), new WebpackShellPlugin({ onBuildStart:[/*'say begin'*/], onBuildEnd:[ 'sed "s/{{buildTime}}/$(date)/g" index.template.html > index.html', //'say end!' //'say world; open "/Applications/Google Chrome.app"', //'sleep 1; say chrome & osascript ./misc/reloadChrome.scpt "http://localhost:63342/web-rtmp/index.html"' ], dev:false} ) ] };