├── README.md
├── LICENSE
├── player.js
├── index.html
├── common.js
├── demux_decode_worker.js
└── flv_demuxer.js
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/player.js:
--------------------------------------------------------------------------------
1 | export class Player {
2 | constructor(options) {
3 | this.demuxDecodeWorker = null;
4 | this.canvas = null;
5 | this.$container = options.container;
6 | this.offscreen = null;
7 | this._initCanvas();
8 | this.showVbps = options.showVbps;
9 |
10 | this.demuxDecodeWorker = new Worker("./demux_decode_worker.js");
11 | this.demuxDecodeWorker.postMessage({ canvas: this.offscreen, type: "init"}, [this.offscreen]);
12 | this.demuxDecodeWorker.addEventListener('message', this.handleMessageFromWorker);
13 | }
14 |
15 | _initCanvas() {
16 | this.canvas = document.createElement('canvas');
17 | this.$container.appendChild(this.canvas);
18 | this.offscreen = this.canvas.transferControlToOffscreen();
19 | }
20 |
21 | play(uri) {
22 | this.demuxDecodeWorker.postMessage({ uri: uri, type: "play"});
23 | if (this.showVbps) {
24 | this.demuxDecodeWorker.postMessage({type: "showVbps"});
25 | }
26 | console.log("Player start play");
27 | }
28 |
29 | pause() {
30 | }
31 |
32 | destroy() {
33 | this.demuxDecodeWorker.postMessage({type: "destroy"});
34 | this.$container.replaceChildren();
35 | console.log("Player destroyed");
36 | }
37 |
38 | handleMessageFromWorker(msg) {
39 | // console.log('incoming message from worker, msg:', msg);
40 | switch (msg.data.type) {
41 | case 'updateStatus':
42 | console.log('updateStatus:', msg.data.status);
43 | break;
44 | default:
45 | throw 'no aTopic on incoming message to ChromeWorker';
46 | }
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | player
9 |
10 |
11 |
12 | Video Player
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
74 |
75 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------