├── LICENSE ├── README.md ├── common.js ├── demux_decode_worker.js ├── flv_demuxer.js ├── index.html └── player.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sun Jun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 播放器基本流程 2 | 3 | 1. websocket加载 4 | 2. flv解析 5 | 3. webcodec解码 6 | 4. canvas渲染 7 | 8 | ## 推流测试命令 9 | 10 | `ffmpeg -re -stream_loop -1 -i test.mp4 -c:v copy -rtsp_transport tcp -f rtsp rtsp://172.28.136.204:8554/live/test121` 11 | 12 | `ws://172.28.136.204:8080/live/test121.live.flv` 13 | 14 | ## 功能已完成 15 | 1. websocket flv协议支持 16 | 2. h264 webcodec解码支持 17 | 3. 播放器基本创建销毁demo 18 | 19 | ## 功能待实现 20 | 1. 全屏功能以及播放器事件回调通知 21 | 2. 播放器断线重连逻辑 22 | 3. h265 硬件解码支持 23 | 4. 代码打包封装 24 | 5. http flv协议支持 25 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | //Player request. 2 | const kPlayVideoReq = 0; 3 | const kPauseVideoReq = 1; 4 | const kStopVideoReq = 2; 5 | 6 | //Player response. 7 | const kPlayVideoRsp = 0; 8 | const kAudioInfo = 1; 9 | const kVideoInfo = 2; 10 | const kAudioData = 3; 11 | const kVideoData = 4; 12 | 13 | //Downloader request. 14 | const kGetFileInfoReq = 0; 15 | const kDownloadFileReq = 1; 16 | const kCloseDownloaderReq = 2; 17 | 18 | //Downloader response. 19 | const kGetFileInfoRsp = 0; 20 | const kFileData = 1; 21 | 22 | //Downloader Protocol. 23 | const kProtoHttp = 0; 24 | const kProtoWebsocket = 1; 25 | 26 | //Decoder request. 27 | const kInitDecoderReq = 0; 28 | const kUninitDecoderReq = 1; 29 | const kOpenDecoderReq = 2; 30 | const kCloseDecoderReq = 3; 31 | const kFeedDataReq = 4; 32 | const kStartDecodingReq = 5; 33 | const kPauseDecodingReq = 6; 34 | const kSeekToReq = 7; 35 | 36 | //Decoder response. 37 | const kInitDecoderRsp = 0; 38 | const kUninitDecoderRsp = 1; 39 | const kOpenDecoderRsp = 2; 40 | const kCloseDecoderRsp = 3; 41 | const kVideoFrame = 4; 42 | const kAudioFrame = 5; 43 | const kStartDecodingRsp = 6; 44 | const kPauseDecodingRsp = 7; 45 | const kDecodeFinishedEvt = 8; 46 | const kRequestDataEvt = 9; 47 | const kSeekToRsp = 10; 48 | 49 | function Logger(module) { 50 | this.module = module; 51 | } 52 | 53 | Logger.prototype.log = function (line) { 54 | console.log("[" + this.currentTimeStr() + "][" + this.module + "]" + line); 55 | } 56 | 57 | Logger.prototype.logError = function (line) { 58 | console.log("[" + this.currentTimeStr() + "][" + this.module + "][ER] " + line); 59 | } 60 | 61 | Logger.prototype.logInfo = function (line) { 62 | console.log("[" + this.currentTimeStr() + "][" + this.module + "][IF] " + line); 63 | } 64 | 65 | Logger.prototype.logDebug = function (line) { 66 | console.log("[" + this.currentTimeStr() + "][" + this.module + "][DT] " + line); 67 | } 68 | 69 | Logger.prototype.currentTimeStr = function () { 70 | var now = new Date(Date.now()); 71 | var year = now.getFullYear(); 72 | var month = now.getMonth() + 1; 73 | var day = now.getDate(); 74 | var hour = now.getHours(); 75 | var min = now.getMinutes(); 76 | var sec = now.getSeconds(); 77 | var ms = now.getMilliseconds(); 78 | return year + "-" + month + "-" + day + " " + hour + ":" + min + ":" + sec + ":" + ms; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /demux_decode_worker.js: -------------------------------------------------------------------------------- 1 | importScripts('./flv_demuxer.js'); 2 | importScripts('./common.js'); 3 | 4 | let startTime = 0; 5 | let frameCount = 0; 6 | let hasConfiged = false; 7 | let decoder = null; 8 | let offscreen = null; 9 | let demuxer = null; 10 | let hasFirstIFrame = false; 11 | let updateStatusInterval = null; 12 | let lastStatusUpdateTime = 0; 13 | let videoBufferLoadedLength = 0; 14 | let showVbps = false; 15 | let vbps = 0; 16 | 17 | const ENCODED_VIDEO_TYPE = { 18 | key: 'key', 19 | delta: 'delta' 20 | }; 21 | 22 | function initPara() { 23 | startTime = 0; 24 | frameCount = 0; 25 | hasConfiged = false; 26 | decoder = null; 27 | offscreen = null; 28 | demuxer = null; 29 | } 30 | 31 | function formatVideoDecoderConfigure(avcC) { 32 | let codecArray = avcC.subarray(1, 4); 33 | let codecString = "avc1."; 34 | for (let j = 0; j < 3; j++) { 35 | let h = codecArray[j].toString(16); 36 | if (h.length < 2) { 37 | h = "0" + h 38 | } 39 | codecString += h 40 | } 41 | 42 | return { 43 | codec: codecString, 44 | description: avcC 45 | } 46 | } 47 | 48 | function getFrameStats() { 49 | let now = performance.now(); 50 | let fps = ""; 51 | 52 | if (frameCount++) { 53 | let elapsed = now - startTime; 54 | fps = " (" + (1000.0 * frameCount / (elapsed)).toFixed(0) + " fps)" 55 | } else { 56 | // This is the first frame. 57 | startTime = now; 58 | } 59 | 60 | return "Extracted " + frameCount + " frames" + fps; 61 | } 62 | 63 | function onVideoData(videoBuffer, timestamp) { 64 | console.log("onVideoData"); 65 | videoBufferLoadedLength += videoBuffer.byteLength; 66 | let uint8Buffer = new Uint8Array(videoBuffer); 67 | 68 | if (!hasConfiged) { 69 | if (uint8Buffer[1] == 0) { 70 | // AVCPacketType == 0, config frame 71 | const videoCodec = (uint8Buffer[0] & 0x0F); 72 | if (videoCodec == 7) { 73 | // avc video codec == 7, h264 74 | const config = formatVideoDecoderConfigure(uint8Buffer.slice(5)); 75 | decoder.configure(config); 76 | offscreen.width = 640; 77 | offscreen.height = 368; 78 | } 79 | } 80 | hasConfiged = true; 81 | } else { 82 | const isIFrame = uint8Buffer[0] >> 4 === 1; 83 | if (!hasFirstIFrame) { 84 | // A key frame is required after configure() or flush(). 85 | if (isIFrame) { 86 | hasFirstIFrame = true; 87 | } else { 88 | // wait first I frame then decode. 89 | return; 90 | } 91 | } 92 | const chunk = new EncodedVideoChunk({ 93 | data: uint8Buffer.slice(5), 94 | timestamp: timestamp, 95 | type: isIFrame ? ENCODED_VIDEO_TYPE.key : ENCODED_VIDEO_TYPE.delta 96 | }) 97 | decoder.decode(chunk); 98 | } 99 | } 100 | 101 | function updateStatus() { 102 | showVbps = true; 103 | updateStatusInterval = setInterval(() =>{ 104 | let now = performance.now(); 105 | let elapsed = now - lastStatusUpdateTime; 106 | vbps = parseInt(videoBufferLoadedLength * 8 / elapsed); 107 | lastStatusUpdateTime = now; 108 | videoBufferLoadedLength = 0; 109 | 110 | self.postMessage({ 111 | type: 'updateStatus', 112 | status: vbps + "kbps" 113 | }); 114 | }, 500); 115 | } 116 | 117 | self.addEventListener('message', function (e) { 118 | if (e.data.type == 'init') { 119 | offscreen = e.data.canvas; 120 | let ctx = offscreen.getContext('2d'); 121 | demuxer = new FLVDemuxer(); 122 | decoder = new VideoDecoder({ 123 | output: frame => { 124 | ctx.drawImage(frame, 0, 0, offscreen.width, offscreen.height); 125 | // Close ASAP. 126 | frame.close(); 127 | // Draw some optional stats. 128 | ctx.font = '35px sans-serif'; 129 | ctx.fillStyle = "#ffffff"; 130 | ctx.fillText(getFrameStats(), 40, 40, offscreen.width); 131 | if (showVbps) 132 | ctx.fillText(vbps + "kbps", 40, 70, offscreen.width); 133 | }, 134 | error: e => console.error(e), 135 | }); 136 | } else if (e.data.type == 'play') { 137 | // let url = "ws://10.20.10.87/live/test121.live.flv"; 138 | // let url = "ws://10.20.10.87:20080/live/test121.flv"; 139 | // let url = "ws://172.23.110.91:8080/live/test121.live.flv"; 140 | console.log("decoder loaded: " + decoder); 141 | demuxer.open(onVideoData, e.data.uri); 142 | } else if (e.data.type == 'destroy') { 143 | clearInterval(updateStatusInterval); 144 | updateStatusInterval = null; 145 | if (demuxer) { 146 | demuxer.close(); 147 | } 148 | if (decoder) { 149 | decoder.close(); 150 | } 151 | initPara(); 152 | } else if (e.data.type == 'showVbps') { 153 | updateStatus(); 154 | } 155 | }) -------------------------------------------------------------------------------- /flv_demuxer.js: -------------------------------------------------------------------------------- 1 | 2 | class FLVDemuxer { 3 | constructor() { 4 | this.uri = null; 5 | this.ws = null; 6 | this.logger = new Logger("Downloader"); 7 | this.outputCount = 0; 8 | this.hexString = ""; 9 | this.hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; 10 | this._onVideoData = null; 11 | this.state = 0; // 0: header, 1: body 12 | this.bodyState = 0; // 0: size, 1: tag header, 2: tag body 13 | this.signature = ''; 14 | this.version = 0x01; 15 | this.hasAudio = false; 16 | this.hasVideo = false; 17 | this.offset = 0x00; 18 | this.type = 0x00; 19 | this.size = 0; 20 | this.timestamp = 0; 21 | this.streamId = 0; 22 | } 23 | 24 | byteToHex(b) { 25 | return this.hexChar[(b >> 4) & 0x0f] + this.hexChar[b & 0x0f]; 26 | } 27 | 28 | parseHeader(buffer) { 29 | let uint8Buffer = new Uint8Array(buffer); 30 | let sliceArray = uint8Buffer.slice(0, 3); 31 | 32 | this.signature = new TextDecoder("utf-8").decode(sliceArray); 33 | //this.signature = uint8Buffer.slice(0, 3).toString('utf-8'); 34 | this.version = uint8Buffer[3]; 35 | if (this.signature != 'FLV' || this.version != 0x01) { 36 | return false; 37 | } 38 | 39 | let flags = uint8Buffer[4]; 40 | this.hasAudio = (flags & 4) >>> 2 == 1; 41 | this.hasVideo = (flags & 1) == 1; 42 | // this.offset = buffer.readUInt32BE(5); 43 | // if (this.offset != 9) { 44 | // return false; 45 | // } 46 | return true; 47 | } 48 | 49 | parseTagHeader(buffer) { 50 | let uint8Buffer = new Uint8Array(buffer); 51 | this.type = uint8Buffer[0]; 52 | // this.size = buffer.readUInt24BE(1); 53 | let ts0 = uint8Buffer[4] << 16 + uint8Buffer[5] << 8 + uint8Buffer[6]; 54 | let ts1 = uint8Buffer[7]; 55 | this.timestamp = (ts1 << 24) | ts0; 56 | // this.streamId = buffer.readUInt24BE(8) >> 8; 57 | } 58 | 59 | demux(data) { 60 | if (this.state == 0) { 61 | if (this.parseHeader(data) == false) { 62 | self.logger.logError("parseHeader error " + data); 63 | //TODO: parse header error 64 | return; 65 | } 66 | this.state = 1; 67 | } else if (this.state == 1) { 68 | if (this.hasVideo == false) { 69 | //TODO: no video return error 70 | return; 71 | } 72 | 73 | if (this.bodyState == 0) { 74 | // previous tag size 75 | this.bodyState = 1; 76 | // this.videoTagHeader = data; 77 | } else if (this.bodyState == 1) { 78 | // tag header 79 | this.bodyState = 2; 80 | this.parseTagHeader(data); 81 | // this.onVideoData(this.videoTagHeader, this.videoTagBody); 82 | } else if (this.bodyState == 2) { 83 | // tag data 84 | if (this.type == 0x09) { 85 | //this is video data 86 | this._onVideoData(data, this.timestamp); 87 | } 88 | this.bodyState = 0; 89 | } 90 | } 91 | } 92 | 93 | open(onVideoData, uri) { 94 | this.uri = uri; 95 | this._onVideoData = onVideoData; 96 | if (this.ws == null) { 97 | this.ws = new WebSocket(this.uri); 98 | this.ws.binaryType = 'arraybuffer'; 99 | 100 | // self.logger.logInfo("data length ", evt.data.byteLength); 101 | let self = this; 102 | this.ws.onopen = function (evt) { 103 | self.logger.logInfo("Ws connected."); 104 | console.log("FLVDemuxer: " + uri); 105 | // self.ws.send(msg); 106 | }; 107 | 108 | this.ws.onerror = function (evt) { 109 | self.logger.logError("Ws connect error " + evt.data); 110 | } 111 | 112 | this.ws.onclose = function (evt) { 113 | self.logger.logError("Ws connect close " + evt.data); 114 | } 115 | 116 | this.ws.onmessage = function (evt) { 117 | self.demux(evt.data); 118 | }; 119 | } 120 | } 121 | close() { 122 | if (this.ws != null) { 123 | this.ws.close(); 124 | this.ws = null; 125 | } 126 | this._onVideoData = null; 127 | this.logger = null; 128 | this.state = 0; 129 | this.bodyState = 0; 130 | this.uri = null; 131 | this.timestamp = 0; 132 | } 133 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |