├── .gitignore ├── .idea └── WebRTMP.iml ├── LICENSE ├── dist ├── webrtmp.js └── webrtmp.js.map ├── docs ├── rtmp_specification_1.0.pdf ├── webrtmp_arch.png └── webrtmp_diagram.png ├── index.html ├── package.json ├── readme.md ├── server ├── etc │ └── nginx │ │ └── nginx.conf └── startWebRTMPProxy.sh ├── src ├── config.js ├── flv │ ├── amf-parser.js │ ├── exp-golomb.js │ └── sps-parser.js ├── formats │ ├── aac-silent.js │ ├── media-info.js │ ├── media-segment-info.js │ ├── mp4-remuxer.js │ └── mp4.js ├── index.js ├── rtmp │ ├── AMF0Object.js │ ├── Chunk.js │ ├── ChunkParser.js │ ├── NetConnection.js │ ├── ProtocolControlMessage.js │ ├── RTMPHandshake.js │ ├── RTMPMediaMessageHandler.js │ ├── RTMPMessage.js │ ├── RTMPMessageHandler.js │ └── UserControlMessage.js ├── utils │ ├── browser.js │ ├── event_emitter.js │ ├── exception.js │ ├── logger.js │ ├── mse-controller.js │ ├── utf8-conv.js │ └── utils.js ├── webrtmp.js └── wss │ ├── WSSConnectionManager.js │ ├── connection.worker.js │ └── webrtmp.controller.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /src/node_modules/ 2 | -------------------------------------------------------------------------------- /.idea/WebRTMP.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /docs/rtmp_specification_1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeinstein/webrtmp/7ef344f8b0fa4dce9dc0462ecd93282d1c192341/docs/rtmp_specification_1.0.pdf -------------------------------------------------------------------------------- /docs/webrtmp_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeinstein/webrtmp/7ef344f8b0fa4dce9dc0462ecd93282d1c192341/docs/webrtmp_arch.png -------------------------------------------------------------------------------- /docs/webrtmp_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeinstein/webrtmp/7ef344f8b0fa4dce9dc0462ecd93282d1c192341/docs/webrtmp_diagram.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebRTMP-Demo 6 | 7 | 17 | 18 | 19 |

WebRTMP-Demo


20 | 21 | 22 | 27 | 33 | 34 | 35 | 38 | 41 | 42 |
23 |
24 |
25 | 26 |
28 |
29 |
30 |
31 |
32 |
36 | 37 | 39 | 40 |
43 | Video is send by: 44 |
ffmpeg -stream_loop -1 -re -i input.mp4 -vf drawtext="fontfile=monofonto.ttf: fontsize=96: box=1: boxcolor=black@0.75: boxborderw=5: fontcolor=white: x=(w-text_w)/2: y=((h-text_h)/2)+((h-text_h)/4): text='%{gmtime\:%M\\\\\:%S}'" -vcodec libx264 -b:v 3M -s 1280x720 -x264-params keyint=120:min-keyint=30:scenecut=0 -preset medium -profile:v main -movflags +faststart -acodec copy -f flv rtmp://127.0.0.1/demo/livetest
45 | 46 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtmp", 3 | "version": "0.1.1", 4 | "description": "", 5 | "private": true, 6 | "author": "Michael Balen", 7 | "license": "Apache-2.0", 8 | "main": "dist/webrtmp.js", 9 | "module": "dist/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/aeinstein/webrtmp" 13 | }, 14 | "scripts": { 15 | "build-develop": "node_modules/.bin/webpack --mode=development", 16 | "build-production": "node_modules/.bin/webpack --mode=production" 17 | }, 18 | "dependencies": { 19 | "webworkify": "^1.5.0", 20 | "worker-loader": "^3.0.8" 21 | }, 22 | "devDependencies": { 23 | "webpack": "^5.58.2", 24 | "webpack-cli": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | WebRTMP 2 | ====== 3 | A HTML5 Flash Video (RTMP) Player written in pure JavaScript without Flash. LONG LIVE RTMP ;-) 4 | 5 | For those who really miss RTMP in Browser, cause HLS sucks. This will be a part of [avideo](https://github.com/aeinstein/avideo). 6 | 7 | ### heavily inspired bei bilibi`s [FlvPlayer](https://github.com/bilibili/flv.js) 8 | 9 | ## Introduction 10 | This project consists of 2 parts. 11 | - Websockify for wrapping TCP in WSS 12 | - WebRTMP Client library 13 | 14 | ## Demo 15 | [https://bunkertv.org/webrtmp/index.html](https://bunkertv.org/webrtmp/index.html) 16 | 17 | ## Features 18 | - RTMP container with H.264 + AAC / MP3 codec playback 19 | - RTMP over Websocket low latency live stream playback <= 3 sec. 20 | - Compatible with Chrome, FireFox, Safari 10, IE11 and Edge 21 | - Extremely low overhead and hardware accelerated by your browser! 22 | - Use of promises 23 | 24 | 25 | ## Getting Started 26 | ### ClientSide: 27 | ```html 28 | 29 | 30 | 45 | ``` 46 | 47 | 48 | 49 | ### ServerSide: 50 | Prerequisites: 51 | ```bash 52 | apt install websockify 53 | ``` 54 | Launch WSS RTMP-Wrapper 55 | (Don't forget to get certificates) 56 | ```bash 57 | websockify -D --cert fullchain.pem --key privkey.pem --ssl-only 9001 127.0.0.1:1935 58 | ``` 59 | 60 | ## TODO 61 | a lot of error and exception handling 62 | 63 | ## Design 64 | #### serverSide 65 | ![arch](docs/webrtmp_diagram.png) 66 | 67 | -------------------------------------------------------------------------------- /server/etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | rtmp { 4 | server { 5 | listen 1935; # Listen on standard RTMP port 6 | chunk_size 4096; 7 | buflen 1s; 8 | notify_method get; 9 | 10 | application demo { 11 | live on; 12 | idle_streams on; 13 | drop_idle_publisher 10s; 14 | publish_notify on; 15 | 16 | wait_video on; 17 | wait_key on; 18 | 19 | # disable consuming the stream from nginx as rtmp 20 | #allow play 127.0.0.1; 21 | allow play all; 22 | allow publich all; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/startWebRTMPProxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 4 | # Copyright (C) 2023 itNOX. All Rights Reserved. 5 | # 6 | # @author Michael Balen 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | # 21 | 22 | websockify -D --cert /home/bunkertv/certs/fullchain.pem --key /home/bunkertv/certs/privkey.pem --ssl-only 9001 127.0.0.1:1935 23 | 24 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "./utils/logger"; 22 | 23 | export const loglevels = { 24 | "RTMPMessage": Log.ERROR, 25 | "RTMPMessageHandler": Log.WARN, 26 | "RTMPMediaMessageHandler": Log.ERROR, 27 | "ChunkParser": Log.WARN, 28 | "RTMPHandshake": Log.ERROR, 29 | "Chunk": Log.OFF, 30 | "MP4Remuxer": Log.ERROR, 31 | "Transmuxer": Log.WARN, 32 | "EventEmitter": Log.DEBUG, 33 | "MSEController": Log.INFO, 34 | "WebRTMP": Log.DEBUG, 35 | "WebRTMP_Controller": Log.WARN, 36 | "WebRTMP Worker": Log.WARN, 37 | "AMF": Log.WARN 38 | } 39 | -------------------------------------------------------------------------------- /src/flv/amf-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | import {IllegalStateException} from "../utils/exception"; 21 | import {decodeUTF8} from "../utils/utf8-conv"; 22 | import Log from "../utils/logger"; 23 | 24 | let le = (function () { 25 | let buf = new ArrayBuffer(2); 26 | (new DataView(buf)).setInt16(0, 256, true); // little-endian write 27 | return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE 28 | })(); 29 | 30 | class AMF { 31 | static TAG = "AMF"; 32 | 33 | /** 34 | * 35 | * @param {Uint8Array} array 36 | * @returns {{}} 37 | */ 38 | static parseScriptData(array) { 39 | Log.d(this.TAG, array); 40 | 41 | let data = {}; 42 | 43 | try { 44 | let name = AMF.parseValue(array); 45 | Log.d(this.TAG, name); 46 | 47 | let value = AMF.parseValue(array.slice(name.size)); 48 | Log.d(this.TAG, value); 49 | 50 | data[name.data] = value.data; 51 | 52 | } catch (e) { 53 | Log.w(this.TAG, e.toString()); 54 | } 55 | 56 | return data; 57 | } 58 | 59 | /** 60 | * 61 | * @param {Uint8Array} array 62 | * @returns {{data: {name: string, value: {}}, size: number, objectEnd: boolean}} 63 | */ 64 | static parseObject(array) { 65 | if (array.length < 3) { 66 | throw new IllegalStateException('Data not enough when parse ScriptDataObject'); 67 | } 68 | let name = AMF.parseString(array); 69 | let value = AMF.parseValue(array.slice(name.size, array.length - name.size)); 70 | let isObjectEnd = value.objectEnd; 71 | 72 | return { 73 | data: { 74 | name: name.data, 75 | value: value.data 76 | }, 77 | size: name.size + value.size, 78 | objectEnd: isObjectEnd 79 | }; 80 | } 81 | 82 | /** 83 | * 84 | * @param {Uint8Array} array 85 | * @returns {{data: {name: string, value: {}}, size: number, objectEnd: boolean}} 86 | */ 87 | static parseVariable(array) { 88 | return AMF.parseObject(array); 89 | } 90 | 91 | /** 92 | * 93 | * @param {Uint8Array} array 94 | * @returns {{data: string, size: number}} 95 | */ 96 | static parseString(array) { 97 | if (array.length < 2) { 98 | throw new IllegalStateException('Data not enough when parse String'); 99 | } 100 | let v = new DataView(array.buffer); 101 | let length = v.getUint16(0, !le); 102 | 103 | let str; 104 | if (length > 0) { 105 | str = decodeUTF8(new Uint8Array(array.slice(2, 2 + length))); 106 | } else { 107 | str = ''; 108 | } 109 | 110 | return { 111 | data: str, 112 | size: 2 + length 113 | }; 114 | } 115 | 116 | static parseLongString(array) { 117 | if (array.length() < 4) { 118 | throw new IllegalStateException('Data not enough when parse LongString'); 119 | } 120 | let v = new DataView(array.buffer); 121 | let length = v.getUint32(0, !le); 122 | 123 | let str; 124 | if (length > 0) { 125 | str = decodeUTF8(new Uint8Array(array.slice(4, 4 +length))); 126 | } else { 127 | str = ''; 128 | } 129 | 130 | return { 131 | data: str, 132 | size: 4 + length 133 | }; 134 | } 135 | 136 | static parseDate(array) { 137 | if (array.length() < 10) { 138 | throw new IllegalStateException('Data size invalid when parse Date'); 139 | } 140 | let v = new DataView(array.buffer); 141 | let timestamp = v.getFloat64(0, !le); 142 | let localTimeOffset = v.getInt16(8, !le); 143 | timestamp += localTimeOffset * 60 * 1000; // get UTC time 144 | 145 | return { 146 | data: new Date(timestamp), 147 | size: 8 + 2 148 | }; 149 | } 150 | 151 | /** 152 | * 153 | * @param {Uint8Array} array 154 | * @returns {{data: {}, size: number, objectEnd: boolean}} 155 | */ 156 | static parseValue(array) { 157 | if (array.length < 1) { 158 | throw new IllegalStateException('Data not enough when parse Value'); 159 | } 160 | 161 | let v = new DataView(array.buffer); 162 | 163 | let offset = 1; 164 | let type = v.getUint8(0); 165 | let value; 166 | let objectEnd = false; 167 | 168 | try { 169 | switch (type) { 170 | case 0: // Number(Double) type 171 | value = v.getFloat64(1, !le); 172 | offset += 8; 173 | break; 174 | case 1: { // Boolean type 175 | let b = v.getUint8(1); 176 | value = b ? true : false; 177 | offset += 1; 178 | break; 179 | } 180 | case 2: { // String type 181 | let amfstr = AMF.parseString(array.slice(1)); 182 | value = amfstr.data; 183 | offset += amfstr.size; 184 | break; 185 | } 186 | case 3: { // Object(s) type 187 | value = {}; 188 | let terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd 189 | if ((v.getUint32(array.length - 4, !le) & 0x00FFFFFF) === 9) { 190 | terminal = 3; 191 | } 192 | while (offset < array.length - 4) { // 4 === type(UI8) + ScriptDataObjectEnd(UI24) 193 | let amfobj = AMF.parseObject(array.slice(offset, offset + array.length - terminal)); 194 | if (amfobj.objectEnd) 195 | break; 196 | value[amfobj.data.name] = amfobj.data.value; 197 | offset += amfobj.size; 198 | } 199 | if (offset <= array.length - 3) { 200 | let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; 201 | if (marker === 9) { 202 | offset += 3; 203 | } 204 | } 205 | break; 206 | } 207 | case 8: { // ECMA array type (Mixed array) 208 | value = {}; 209 | offset += 4; // ECMAArrayLength(UI32) 210 | let terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd 211 | if ((v.getUint32(array.length - 4, !le) & 0x00FFFFFF) === 9) { 212 | terminal = 3; 213 | } 214 | while (offset < array.length - 8) { // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24) 215 | let amfvar = AMF.parseVariable(array.slice(offset, offset + array.length - terminal)); 216 | if (amfvar.objectEnd) 217 | break; 218 | value[amfvar.data.name] = amfvar.data.value; 219 | offset += amfvar.size; 220 | } 221 | if (offset <= array.length - 3) { 222 | let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; 223 | if (marker === 9) { 224 | offset += 3; 225 | } 226 | } 227 | break; 228 | } 229 | case 9: // ScriptDataObjectEnd 230 | value = undefined; 231 | offset = 1; 232 | objectEnd = true; 233 | break; 234 | case 10: { // Strict array type 235 | // ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf 236 | value = []; 237 | let strictArrayLength = v.getUint32(1, !le); 238 | offset += 4; 239 | for (let i = 0; i < strictArrayLength; i++) { 240 | let val = AMF.parseValue(array.slice(offset, array.length)); 241 | value.push(val.data); 242 | offset += val.size; 243 | } 244 | break; 245 | } 246 | case 11: { // Date type 247 | let date = AMF.parseDate(array.slice(1)); 248 | value = date.data; 249 | offset += date.size; 250 | break; 251 | } 252 | case 12: { // Long string type 253 | let amfLongStr = AMF.parseString(array.slice(1)); 254 | value = amfLongStr.data; 255 | offset += amfLongStr.size; 256 | break; 257 | } 258 | default: 259 | // ignore and skip 260 | offset = array.length; 261 | Log.w(this.TAG, 'Unsupported AMF value type ' + type); 262 | } 263 | } catch (e) { 264 | Log.e(this.TAG, e.toString()); 265 | } 266 | 267 | return { 268 | data: value, 269 | size: offset, 270 | objectEnd: objectEnd 271 | }; 272 | } 273 | } 274 | 275 | export default AMF; 276 | -------------------------------------------------------------------------------- /src/flv/exp-golomb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Exponential-Golomb buffer decoder 20 | import {IllegalStateException, InvalidArgumentException} from "../utils/exception"; 21 | 22 | class ExpGolomb { 23 | 24 | constructor(uint8array) { 25 | this.TAG = 'ExpGolomb'; 26 | 27 | this._buffer = uint8array; 28 | this._buffer_index = 0; 29 | this._total_bytes = uint8array.byteLength; 30 | this._total_bits = uint8array.byteLength * 8; 31 | this._current_word = 0; 32 | this._current_word_bits_left = 0; 33 | } 34 | 35 | destroy() { 36 | this._buffer = null; 37 | } 38 | 39 | _fillCurrentWord() { 40 | let buffer_bytes_left = this._total_bytes - this._buffer_index; 41 | if (buffer_bytes_left <= 0) 42 | throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available'); 43 | 44 | let bytes_read = Math.min(4, buffer_bytes_left); 45 | let word = new Uint8Array(4); 46 | word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read)); 47 | this._current_word = new DataView(word.buffer).getUint32(0, false); 48 | 49 | this._buffer_index += bytes_read; 50 | this._current_word_bits_left = bytes_read * 8; 51 | } 52 | 53 | readBits(bits) { 54 | if (bits > 32) 55 | throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!'); 56 | 57 | if (bits <= this._current_word_bits_left) { 58 | let result = this._current_word >>> (32 - bits); 59 | this._current_word <<= bits; 60 | this._current_word_bits_left -= bits; 61 | return result; 62 | } 63 | 64 | let result = this._current_word_bits_left ? this._current_word : 0; 65 | result = result >>> (32 - this._current_word_bits_left); 66 | let bits_need_left = bits - this._current_word_bits_left; 67 | 68 | this._fillCurrentWord(); 69 | let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left); 70 | 71 | let result2 = this._current_word >>> (32 - bits_read_next); 72 | this._current_word <<= bits_read_next; 73 | this._current_word_bits_left -= bits_read_next; 74 | 75 | result = (result << bits_read_next) | result2; 76 | return result; 77 | } 78 | 79 | readBool() { 80 | return this.readBits(1) === 1; 81 | } 82 | 83 | readByte() { 84 | return this.readBits(8); 85 | } 86 | 87 | _skipLeadingZero() { 88 | let zero_count; 89 | for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) { 90 | if (0 !== (this._current_word & (0x80000000 >>> zero_count))) { 91 | this._current_word <<= zero_count; 92 | this._current_word_bits_left -= zero_count; 93 | return zero_count; 94 | } 95 | } 96 | this._fillCurrentWord(); 97 | return zero_count + this._skipLeadingZero(); 98 | } 99 | 100 | readUEG() { // unsigned exponential golomb 101 | let leading_zeros = this._skipLeadingZero(); 102 | return this.readBits(leading_zeros + 1) - 1; 103 | } 104 | 105 | readSEG() { // signed exponential golomb 106 | let value = this.readUEG(); 107 | if (value & 0x01) { 108 | return (value + 1) >>> 1; 109 | } else { 110 | return -1 * (value >>> 1); 111 | } 112 | } 113 | 114 | } 115 | 116 | export default ExpGolomb; 117 | -------------------------------------------------------------------------------- /src/flv/sps-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | 21 | import ExpGolomb from "./exp-golomb"; 22 | 23 | class SPSParser { 24 | static _ebsp2rbsp(uint8array) { 25 | let src = uint8array; 26 | let src_length = src.byteLength; 27 | let dst = new Uint8Array(src_length); 28 | let dst_idx = 0; 29 | 30 | for (let i = 0; i < src_length; i++) { 31 | if (i >= 2) { 32 | // Unescape: Skip 0x03 after 00 00 33 | if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) { 34 | continue; 35 | } 36 | } 37 | dst[dst_idx] = src[i]; 38 | dst_idx++; 39 | } 40 | 41 | return new Uint8Array(dst.buffer, 0, dst_idx); 42 | } 43 | 44 | static parseSPS(uint8array) { 45 | let rbsp = SPSParser._ebsp2rbsp(uint8array); 46 | let gb = new ExpGolomb(rbsp); 47 | 48 | gb.readByte(); 49 | let profile_idc = gb.readByte(); // profile_idc 50 | gb.readByte(); // constraint_set_flags[5] + reserved_zero[3] 51 | let level_idc = gb.readByte(); // level_idc 52 | gb.readUEG(); // seq_parameter_set_id 53 | 54 | let profile_string = SPSParser.getProfileString(profile_idc); 55 | let level_string = SPSParser.getLevelString(level_idc); 56 | let chroma_format_idc = 1; 57 | let chroma_format = 420; 58 | let chroma_format_table = [0, 420, 422, 444]; 59 | let bit_depth = 8; 60 | 61 | if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || 62 | profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || 63 | profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || 64 | profile_idc === 138 || profile_idc === 144) { 65 | 66 | chroma_format_idc = gb.readUEG(); 67 | if (chroma_format_idc === 3) { 68 | gb.readBits(1); // separate_colour_plane_flag 69 | } 70 | if (chroma_format_idc <= 3) { 71 | chroma_format = chroma_format_table[chroma_format_idc]; 72 | } 73 | 74 | bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8 75 | gb.readUEG(); // bit_depth_chroma_minus8 76 | gb.readBits(1); // qpprime_y_zero_transform_bypass_flag 77 | if (gb.readBool()) { // seq_scaling_matrix_present_flag 78 | let scaling_list_count = (chroma_format_idc !== 3) ? 8 : 12; 79 | for (let i = 0; i < scaling_list_count; i++) { 80 | if (gb.readBool()) { // seq_scaling_list_present_flag 81 | if (i < 6) { 82 | SPSParser._skipScalingList(gb, 16); 83 | } else { 84 | SPSParser._skipScalingList(gb, 64); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | gb.readUEG(); // log2_max_frame_num_minus4 91 | let pic_order_cnt_type = gb.readUEG(); 92 | if (pic_order_cnt_type === 0) { 93 | gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4 94 | } else if (pic_order_cnt_type === 1) { 95 | gb.readBits(1); // delta_pic_order_always_zero_flag 96 | gb.readSEG(); // offset_for_non_ref_pic 97 | gb.readSEG(); // offset_for_top_to_bottom_field 98 | let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG(); 99 | for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) { 100 | gb.readSEG(); // offset_for_ref_frame 101 | } 102 | } 103 | let ref_frames = gb.readUEG(); // max_num_ref_frames 104 | gb.readBits(1); // gaps_in_frame_num_value_allowed_flag 105 | 106 | let pic_width_in_mbs_minus1 = gb.readUEG(); 107 | let pic_height_in_map_units_minus1 = gb.readUEG(); 108 | 109 | let frame_mbs_only_flag = gb.readBits(1); 110 | if (frame_mbs_only_flag === 0) { 111 | gb.readBits(1); // mb_adaptive_frame_field_flag 112 | } 113 | gb.readBits(1); // direct_8x8_inference_flag 114 | 115 | let frame_crop_left_offset = 0; 116 | let frame_crop_right_offset = 0; 117 | let frame_crop_top_offset = 0; 118 | let frame_crop_bottom_offset = 0; 119 | 120 | let frame_cropping_flag = gb.readBool(); 121 | if (frame_cropping_flag) { 122 | frame_crop_left_offset = gb.readUEG(); 123 | frame_crop_right_offset = gb.readUEG(); 124 | frame_crop_top_offset = gb.readUEG(); 125 | frame_crop_bottom_offset = gb.readUEG(); 126 | } 127 | 128 | let sar_width = 1, sar_height = 1; 129 | let fps = 0, fps_fixed = true, fps_num = 0, fps_den = 0; 130 | 131 | let vui_parameters_present_flag = gb.readBool(); 132 | if (vui_parameters_present_flag) { 133 | if (gb.readBool()) { // aspect_ratio_info_present_flag 134 | let aspect_ratio_idc = gb.readByte(); 135 | let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; 136 | let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; 137 | 138 | if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { 139 | sar_width = sar_w_table[aspect_ratio_idc - 1]; 140 | sar_height = sar_h_table[aspect_ratio_idc - 1]; 141 | } else if (aspect_ratio_idc === 255) { 142 | sar_width = gb.readByte() << 8 | gb.readByte(); 143 | sar_height = gb.readByte() << 8 | gb.readByte(); 144 | } 145 | } 146 | 147 | if (gb.readBool()) { // overscan_info_present_flag 148 | gb.readBool(); // overscan_appropriate_flag 149 | } 150 | if (gb.readBool()) { // video_signal_type_present_flag 151 | gb.readBits(4); // video_format & video_full_range_flag 152 | if (gb.readBool()) { // colour_description_present_flag 153 | gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients 154 | } 155 | } 156 | if (gb.readBool()) { // chroma_loc_info_present_flag 157 | gb.readUEG(); // chroma_sample_loc_type_top_field 158 | gb.readUEG(); // chroma_sample_loc_type_bottom_field 159 | } 160 | if (gb.readBool()) { // timing_info_present_flag 161 | let num_units_in_tick = gb.readBits(32); 162 | let time_scale = gb.readBits(32); 163 | fps_fixed = gb.readBool(); // fixed_frame_rate_flag 164 | 165 | fps_num = time_scale; 166 | fps_den = num_units_in_tick * 2; 167 | fps = fps_num / fps_den; 168 | } 169 | } 170 | 171 | let sarScale = 1; 172 | if (sar_width !== 1 || sar_height !== 1) { 173 | sarScale = sar_width / sar_height; 174 | } 175 | 176 | let crop_unit_x = 0, crop_unit_y = 0; 177 | if (chroma_format_idc === 0) { 178 | crop_unit_x = 1; 179 | crop_unit_y = 2 - frame_mbs_only_flag; 180 | } else { 181 | let sub_wc = (chroma_format_idc === 3) ? 1 : 2; 182 | let sub_hc = (chroma_format_idc === 1) ? 2 : 1; 183 | crop_unit_x = sub_wc; 184 | crop_unit_y = sub_hc * (2 - frame_mbs_only_flag); 185 | } 186 | 187 | let codec_width = (pic_width_in_mbs_minus1 + 1) * 16; 188 | let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16); 189 | 190 | codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x; 191 | codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y; 192 | 193 | let present_width = Math.ceil(codec_width * sarScale); 194 | 195 | gb.destroy(); 196 | gb = null; 197 | 198 | return { 199 | profile_string: profile_string, // baseline, high, high10, ... 200 | level_string: level_string, // 3, 3.1, 4, 4.1, 5, 5.1, ... 201 | bit_depth: bit_depth, // 8bit, 10bit, ... 202 | ref_frames: ref_frames, 203 | chroma_format: chroma_format, // 4:2:0, 4:2:2, ... 204 | chroma_format_string: SPSParser.getChromaFormatString(chroma_format), 205 | 206 | frame_rate: { 207 | fixed: fps_fixed, 208 | fps: fps, 209 | fps_den: fps_den, 210 | fps_num: fps_num 211 | }, 212 | 213 | sar_ratio: { 214 | width: sar_width, 215 | height: sar_height 216 | }, 217 | 218 | codec_size: { 219 | width: codec_width, 220 | height: codec_height 221 | }, 222 | 223 | present_size: { 224 | width: present_width, 225 | height: codec_height 226 | } 227 | }; 228 | } 229 | 230 | static _skipScalingList(gb, count) { 231 | let last_scale = 8, next_scale = 8; 232 | let delta_scale = 0; 233 | for (let i = 0; i < count; i++) { 234 | if (next_scale !== 0) { 235 | delta_scale = gb.readSEG(); 236 | next_scale = (last_scale + delta_scale + 256) % 256; 237 | } 238 | last_scale = (next_scale === 0) ? last_scale : next_scale; 239 | } 240 | } 241 | 242 | static getProfileString(profile_idc) { 243 | switch (profile_idc) { 244 | case 66: 245 | return 'Baseline'; 246 | case 77: 247 | return 'Main'; 248 | case 88: 249 | return 'Extended'; 250 | case 100: 251 | return 'High'; 252 | case 110: 253 | return 'High10'; 254 | case 122: 255 | return 'High422'; 256 | case 244: 257 | return 'High444'; 258 | default: 259 | return 'Unknown'; 260 | } 261 | } 262 | 263 | static getLevelString(level_idc) { 264 | return (level_idc / 10).toFixed(1); 265 | } 266 | 267 | static getChromaFormatString(chroma) { 268 | switch (chroma) { 269 | case 420: 270 | return '4:2:0'; 271 | case 422: 272 | return '4:2:2'; 273 | case 444: 274 | return '4:4:4'; 275 | default: 276 | return 'Unknown'; 277 | } 278 | } 279 | 280 | } 281 | 282 | export default SPSParser; 283 | -------------------------------------------------------------------------------- /src/formats/aac-silent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * This file is modified from dailymotion's hls.js library (hls.js/src/helper/aac.js) 5 | * @author zheng qian 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | class AAC { 21 | static getSilentFrame(codec, channelCount) { 22 | if (codec === 'mp4a.40.2') { 23 | // handle LC-AAC 24 | if (channelCount === 1) { 25 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); 26 | } else if (channelCount === 2) { 27 | return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); 28 | } else if (channelCount === 3) { 29 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); 30 | } else if (channelCount === 4) { 31 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); 32 | } else if (channelCount === 5) { 33 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); 34 | } else if (channelCount === 6) { 35 | return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); 36 | } 37 | } else { 38 | // handle HE-AAC (mp4a.40.5 / mp4a.40.29) 39 | if (channelCount === 1) { 40 | // ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 41 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 42 | } else if (channelCount === 2) { 43 | // ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 44 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 45 | } else if (channelCount === 3) { 46 | // ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 47 | return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 48 | } 49 | } 50 | return null; 51 | } 52 | 53 | } 54 | 55 | export default AAC; 56 | -------------------------------------------------------------------------------- /src/formats/media-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | class MediaInfo { 20 | 21 | constructor() { 22 | this.mimeType = null; 23 | this.duration = null; 24 | 25 | this.hasAudio = null; 26 | this.hasVideo = null; 27 | this.audioCodec = null; 28 | this.videoCodec = null; 29 | this.audioDataRate = null; 30 | this.videoDataRate = null; 31 | 32 | this.audioSampleRate = null; 33 | this.audioChannelCount = null; 34 | 35 | this.width = null; 36 | this.height = null; 37 | this.fps = null; 38 | this.profile = null; 39 | this.level = null; 40 | this.refFrames = null; 41 | this.chromaFormat = null; 42 | this.sarNum = null; 43 | this.sarDen = null; 44 | 45 | this.metadata = null; 46 | this.segments = null; // MediaInfo[] 47 | this.segmentCount = null; 48 | this.hasKeyframesIndex = null; 49 | this.keyframesIndex = null; 50 | } 51 | 52 | isComplete() { 53 | let audioInfoComplete = (this.hasAudio === false) || 54 | (this.hasAudio === true && 55 | this.audioCodec != null && 56 | this.audioSampleRate != null && 57 | this.audioChannelCount != null); 58 | 59 | let videoInfoComplete = (this.hasVideo === false) || 60 | (this.hasVideo === true && 61 | this.videoCodec != null && 62 | this.width != null && 63 | this.height != null && 64 | this.fps != null && 65 | this.profile != null && 66 | this.level != null && 67 | this.refFrames != null && 68 | this.chromaFormat != null && 69 | this.sarNum != null && 70 | this.sarDen != null); 71 | 72 | // keyframesIndex may not be present 73 | return this.mimeType != null && 74 | this.duration != null && 75 | this.metadata != null && 76 | this.hasKeyframesIndex != null && 77 | audioInfoComplete && 78 | videoInfoComplete; 79 | } 80 | 81 | isSeekable() { 82 | return this.hasKeyframesIndex === true; 83 | } 84 | 85 | getNearestKeyframe(milliseconds) { 86 | if (this.keyframesIndex == null) { 87 | return null; 88 | } 89 | 90 | let table = this.keyframesIndex; 91 | let keyframeIdx = this._search(table.times, milliseconds); 92 | 93 | return { 94 | index: keyframeIdx, 95 | milliseconds: table.times[keyframeIdx], 96 | fileposition: table.filepositions[keyframeIdx] 97 | }; 98 | } 99 | 100 | _search(list, value) { 101 | let idx = 0; 102 | 103 | let last = list.length - 1; 104 | let mid = 0; 105 | let lbound = 0; 106 | let ubound = last; 107 | 108 | if (value < list[0]) { 109 | idx = 0; 110 | lbound = ubound + 1; // skip search 111 | } 112 | 113 | while (lbound <= ubound) { 114 | mid = lbound + Math.floor((ubound - lbound) / 2); 115 | if (mid === last || (value >= list[mid] && value < list[mid + 1])) { 116 | idx = mid; 117 | break; 118 | } else if (list[mid] < value) { 119 | lbound = mid + 1; 120 | } else { 121 | ubound = mid - 1; 122 | } 123 | } 124 | 125 | return idx; 126 | } 127 | 128 | } 129 | 130 | export default MediaInfo; 131 | -------------------------------------------------------------------------------- /src/formats/media-segment-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Represents an media sample (audio / video) 20 | export class SampleInfo { 21 | 22 | constructor(dts, pts, duration, originalDts, isSync) { 23 | this.dts = dts; 24 | this.pts = pts; 25 | this.duration = duration; 26 | this.originalDts = originalDts; 27 | this.isSyncPoint = isSync; 28 | this.fileposition = null; 29 | } 30 | 31 | } 32 | 33 | // Media Segment concept is defined in Media Source Extensions spec. 34 | // Particularly in ISO BMFF format, an Media Segment contains a moof box followed by a mdat box. 35 | export class MediaSegmentInfo { 36 | 37 | constructor() { 38 | this.beginDts = 0; 39 | this.endDts = 0; 40 | this.beginPts = 0; 41 | this.endPts = 0; 42 | this.originalBeginDts = 0; 43 | this.originalEndDts = 0; 44 | this.syncPoints = []; // SampleInfo[n], for video IDR frames only 45 | this.firstSample = null; // SampleInfo 46 | this.lastSample = null; // SampleInfo 47 | } 48 | 49 | appendSyncPoint(sampleInfo) { // also called Random Access Point 50 | sampleInfo.isSyncPoint = true; 51 | this.syncPoints.push(sampleInfo); 52 | } 53 | 54 | } 55 | 56 | // Ordered list for recording video IDR frames, sorted by originalDts 57 | export class IDRSampleList { 58 | 59 | constructor() { 60 | this._list = []; 61 | } 62 | 63 | clear() { 64 | this._list = []; 65 | } 66 | 67 | appendArray(syncPoints) { 68 | let list = this._list; 69 | 70 | if (syncPoints.length === 0) { 71 | return; 72 | } 73 | 74 | if (list.length > 0 && syncPoints[0].originalDts < list[list.length - 1].originalDts) { 75 | this.clear(); 76 | } 77 | 78 | Array.prototype.push.apply(list, syncPoints); 79 | } 80 | 81 | getLastSyncPointBeforeDts(dts) { 82 | if (this._list.length === 0) { 83 | return null; 84 | } 85 | 86 | let list = this._list; 87 | let idx = 0; 88 | let last = list.length - 1; 89 | let mid = 0; 90 | let lbound = 0; 91 | let ubound = last; 92 | 93 | if (dts < list[0].dts) { 94 | idx = 0; 95 | lbound = ubound + 1; 96 | } 97 | 98 | while (lbound <= ubound) { 99 | mid = lbound + Math.floor((ubound - lbound) / 2); 100 | if (mid === last || (dts >= list[mid].dts && dts < list[mid + 1].dts)) { 101 | idx = mid; 102 | break; 103 | } else if (list[mid].dts < dts) { 104 | lbound = mid + 1; 105 | } else { 106 | ubound = mid - 1; 107 | } 108 | } 109 | return this._list[idx]; 110 | } 111 | 112 | } 113 | 114 | // Data structure for recording information of media segments in single track. 115 | export class MediaSegmentInfoList { 116 | 117 | constructor(type) { 118 | this._type = type; 119 | this._list = []; 120 | this._lastAppendLocation = -1; // cached last insert location 121 | } 122 | 123 | get type() { 124 | return this._type; 125 | } 126 | 127 | get length() { 128 | return this._list.length; 129 | } 130 | 131 | isEmpty() { 132 | return this._list.length === 0; 133 | } 134 | 135 | clear() { 136 | this._list = []; 137 | this._lastAppendLocation = -1; 138 | } 139 | 140 | _searchNearestSegmentBefore(originalBeginDts) { 141 | let list = this._list; 142 | if (list.length === 0) { 143 | return -2; 144 | } 145 | let last = list.length - 1; 146 | let mid = 0; 147 | let lbound = 0; 148 | let ubound = last; 149 | 150 | let idx = 0; 151 | 152 | if (originalBeginDts < list[0].originalBeginDts) { 153 | idx = -1; 154 | return idx; 155 | } 156 | 157 | while (lbound <= ubound) { 158 | mid = lbound + Math.floor((ubound - lbound) / 2); 159 | if (mid === last || (originalBeginDts > list[mid].lastSample.originalDts && 160 | (originalBeginDts < list[mid + 1].originalBeginDts))) { 161 | idx = mid; 162 | break; 163 | } else if (list[mid].originalBeginDts < originalBeginDts) { 164 | lbound = mid + 1; 165 | } else { 166 | ubound = mid - 1; 167 | } 168 | } 169 | return idx; 170 | } 171 | 172 | _searchNearestSegmentAfter(originalBeginDts) { 173 | return this._searchNearestSegmentBefore(originalBeginDts) + 1; 174 | } 175 | 176 | append(mediaSegmentInfo) { 177 | let list = this._list; 178 | let msi = mediaSegmentInfo; 179 | let lastAppendIdx = this._lastAppendLocation; 180 | let insertIdx = 0; 181 | 182 | if (lastAppendIdx !== -1 && lastAppendIdx < list.length && 183 | msi.originalBeginDts >= list[lastAppendIdx].lastSample.originalDts && 184 | ((lastAppendIdx === list.length - 1) || 185 | (lastAppendIdx < list.length - 1 && 186 | msi.originalBeginDts < list[lastAppendIdx + 1].originalBeginDts))) { 187 | insertIdx = lastAppendIdx + 1; // use cached location idx 188 | } else { 189 | if (list.length > 0) { 190 | insertIdx = this._searchNearestSegmentBefore(msi.originalBeginDts) + 1; 191 | } 192 | } 193 | 194 | this._lastAppendLocation = insertIdx; 195 | this._list.splice(insertIdx, 0, msi); 196 | } 197 | 198 | getLastSegmentBefore(originalBeginDts) { 199 | let idx = this._searchNearestSegmentBefore(originalBeginDts); 200 | if (idx >= 0) { 201 | return this._list[idx]; 202 | } else { // -1 203 | return null; 204 | } 205 | } 206 | 207 | getLastSampleBefore(originalBeginDts) { 208 | let segment = this.getLastSegmentBefore(originalBeginDts); 209 | if (segment != null) { 210 | return segment.lastSample; 211 | } else { 212 | return null; 213 | } 214 | } 215 | 216 | getLastSyncPointBefore(originalBeginDts) { 217 | let segmentIdx = this._searchNearestSegmentBefore(originalBeginDts); 218 | let syncPoints = this._list[segmentIdx].syncPoints; 219 | while (syncPoints.length === 0 && segmentIdx > 0) { 220 | segmentIdx--; 221 | syncPoints = this._list[segmentIdx].syncPoints; 222 | } 223 | if (syncPoints.length > 0) { 224 | return syncPoints[syncPoints.length - 1]; 225 | } else { 226 | return null; 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/formats/mp4-remuxer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | // Fragmented mp4 remuxer 21 | import MP4 from "./mp4"; 22 | import {MediaSegmentInfo, MediaSegmentInfoList, SampleInfo} from "./media-segment-info"; 23 | import AAC from "./aac-silent"; 24 | import {IllegalStateException} from "../utils/exception"; 25 | import Browser from "../utils/browser"; 26 | import Log from "../utils/logger"; 27 | 28 | class MP4Remuxer { 29 | TAG = 'MP4Remuxer' 30 | 31 | constructor(config) { 32 | this._config = config; 33 | this._isLive = (config.isLive === true); 34 | 35 | this._dtsBase = -1; 36 | this._dtsBaseInited = false; 37 | this._audioDtsBase = Infinity; 38 | this._videoDtsBase = Infinity; 39 | this._audioNextDts = undefined; 40 | this._videoNextDts = undefined; 41 | this._audioStashedLastSample = null; 42 | this._videoStashedLastSample = null; 43 | 44 | this._audioMeta = null; 45 | this._videoMeta = null; 46 | 47 | this._audioSegmentInfoList = new MediaSegmentInfoList('audio'); 48 | this._videoSegmentInfoList = new MediaSegmentInfoList('video'); 49 | 50 | this._onInitSegment = null; 51 | this._onMediaSegment = null; 52 | 53 | // Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment 54 | // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 55 | this._forceFirstIDR = (Browser.chrome && 56 | (Browser.version.major < 50 || 57 | (Browser.version.major === 50 && Browser.version.build < 2661))) ? true : false; 58 | 59 | // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking 60 | // Make audio beginDts equals with video beginDts, in order to fix seek freeze 61 | this._fillSilentAfterSeek = (Browser.msedge || Browser.msie); 62 | 63 | // While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ... 64 | this._mp3UseMpegAudio = !Browser.firefox; 65 | 66 | this._fillAudioTimestampGap = this._config.fixAudioTimestampGap; 67 | } 68 | 69 | destroy() { 70 | this._dtsBase = -1; 71 | this._dtsBaseInited = false; 72 | this._audioMeta = null; 73 | this._videoMeta = null; 74 | this._audioSegmentInfoList.clear(); 75 | this._audioSegmentInfoList = null; 76 | this._videoSegmentInfoList.clear(); 77 | this._videoSegmentInfoList = null; 78 | this._onInitSegment = null; 79 | this._onMediaSegment = null; 80 | } 81 | 82 | get onInitSegment() { 83 | return this._onInitSegment; 84 | } 85 | 86 | set onInitSegment(callback) { 87 | this._onInitSegment = callback; 88 | } 89 | 90 | get onMediaSegment() { 91 | return this._onMediaSegment; 92 | } 93 | 94 | set onMediaSegment(callback) { 95 | this._onMediaSegment = callback; 96 | } 97 | 98 | insertDiscontinuity() { 99 | this._audioNextDts = this._videoNextDts = undefined; 100 | } 101 | 102 | seek(originalDts) { 103 | this._audioStashedLastSample = null; 104 | this._videoStashedLastSample = null; 105 | this._videoSegmentInfoList.clear(); 106 | this._audioSegmentInfoList.clear(); 107 | } 108 | 109 | remux(audioTrack, videoTrack) { 110 | if (!this._onMediaSegment) { 111 | throw new IllegalStateException('MP4Remuxer: onMediaSegment callback must be specificed!'); 112 | } 113 | if (!this._dtsBaseInited) { 114 | this._calculateDtsBase(audioTrack, videoTrack); 115 | } 116 | this._remuxVideo(videoTrack); 117 | this._remuxAudio(audioTrack); 118 | } 119 | 120 | _onTrackMetadataReceived(type, metadata) { 121 | Log.i(this.TAG, "_onTrackMetadataReceived"); 122 | let metabox = null; 123 | 124 | let container = 'mp4'; 125 | let codec = metadata.codec; 126 | 127 | if (type === 'audio') { 128 | this._audioMeta = metadata; 129 | if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) { 130 | // 'audio/mpeg' for MP3 audio track 131 | container = 'mpeg'; 132 | codec = ''; 133 | metabox = new Uint8Array(0); 134 | } else { 135 | // 'audio/mp4, codecs="codec"' 136 | metabox = MP4.generateInitSegment(metadata); 137 | } 138 | } else if (type === 'video') { 139 | this._videoMeta = metadata; 140 | metabox = MP4.generateInitSegment(metadata); 141 | } else { 142 | return; 143 | } 144 | 145 | // dispatch metabox (Initialization Segment) 146 | if (!this._onInitSegment) { 147 | throw new IllegalStateException('MP4Remuxer: onInitSegment callback must be specified!'); 148 | } 149 | this._onInitSegment(type, { 150 | type: type, 151 | data: metabox.buffer, 152 | codec: codec, 153 | container: `${type}/${container}`, 154 | mediaDuration: metadata.duration // in timescale 1000 (milliseconds) 155 | }); 156 | } 157 | 158 | _calculateDtsBase(audioTrack, videoTrack) { 159 | if (this._dtsBaseInited) { 160 | return; 161 | } 162 | 163 | if (audioTrack.samples && audioTrack.samples.length) { 164 | this._audioDtsBase = audioTrack.samples[0].dts; 165 | } 166 | if (videoTrack.samples && videoTrack.samples.length) { 167 | this._videoDtsBase = videoTrack.samples[0].dts; 168 | } 169 | 170 | this._dtsBase = Math.min(this._audioDtsBase, this._videoDtsBase); 171 | this._dtsBaseInited = true; 172 | } 173 | 174 | flushStashedSamples() { 175 | let videoSample = this._videoStashedLastSample; 176 | let audioSample = this._audioStashedLastSample; 177 | 178 | let videoTrack = { 179 | type: 'video', 180 | id: 1, 181 | sequenceNumber: 0, 182 | samples: [], 183 | length: 0 184 | }; 185 | 186 | if (videoSample != null) { 187 | videoTrack.samples.push(videoSample); 188 | videoTrack.length = videoSample.length; 189 | } 190 | 191 | let audioTrack = { 192 | type: 'audio', 193 | id: 2, 194 | sequenceNumber: 0, 195 | samples: [], 196 | length: 0 197 | }; 198 | 199 | if (audioSample != null) { 200 | audioTrack.samples.push(audioSample); 201 | audioTrack.length = audioSample.length; 202 | } 203 | 204 | this._videoStashedLastSample = null; 205 | this._audioStashedLastSample = null; 206 | 207 | this._remuxVideo(videoTrack, true); 208 | this._remuxAudio(audioTrack, true); 209 | } 210 | 211 | _remuxAudio(audioTrack, force) { 212 | Log.i(this.TAG, "_remuxAudio"); 213 | if (this._audioMeta == null) { 214 | Log.w(this.TAG, "no audioMeta"); 215 | return; 216 | } 217 | 218 | let track = audioTrack; 219 | let samples = track.samples; 220 | let dtsCorrection = undefined; 221 | let firstDts = -1, lastDts = -1, lastPts = -1; 222 | let refSampleDuration = this._audioMeta.refSampleDuration; 223 | 224 | let mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio; 225 | let firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined; 226 | 227 | let insertPrefixSilentFrame = false; 228 | 229 | if (!samples || samples.length === 0) { 230 | Log.w(this.TAG, "no samples"); 231 | return; 232 | } 233 | if (samples.length === 1 && !force) { 234 | // If [sample count in current batch] === 1 && (force != true) 235 | // Ignore and keep in demuxer's queue 236 | Log.w(this.TAG, "1 sample"); 237 | return; 238 | } // else if (force === true) do remux 239 | 240 | let offset = 0; 241 | let mdatbox = null; 242 | let mdatBytes = 0; 243 | 244 | // calculate initial mdat size 245 | if (mpegRawTrack) { 246 | // for raw mpeg buffer 247 | offset = 0; 248 | mdatBytes = track.length; 249 | } else { 250 | // for fmp4 mdat box 251 | offset = 8; // size + type 252 | mdatBytes = 8 + track.length; 253 | } 254 | 255 | 256 | let lastSample = null; 257 | 258 | // Pop the lastSample and waiting for stash 259 | if (samples.length > 1) { 260 | lastSample = samples.pop(); 261 | mdatBytes -= lastSample.length; 262 | } 263 | 264 | // Insert [stashed lastSample in the previous batch] to the front 265 | if (this._audioStashedLastSample != null) { 266 | let sample = this._audioStashedLastSample; 267 | this._audioStashedLastSample = null; 268 | samples.unshift(sample); 269 | mdatBytes += sample.length; 270 | } 271 | 272 | // Stash the lastSample of current batch, waiting for next batch 273 | if (lastSample != null) { 274 | this._audioStashedLastSample = lastSample; 275 | } 276 | 277 | 278 | let firstSampleOriginalDts = samples[0].dts - this._dtsBase; 279 | 280 | // calculate dtsCorrection 281 | if (this._audioNextDts) { 282 | dtsCorrection = firstSampleOriginalDts - this._audioNextDts; 283 | } else { // this._audioNextDts == undefined 284 | if (this._audioSegmentInfoList.isEmpty()) { 285 | dtsCorrection = 0; 286 | if (this._fillSilentAfterSeek && !this._videoSegmentInfoList.isEmpty()) { 287 | if (this._audioMeta.originalCodec !== 'mp3') { 288 | insertPrefixSilentFrame = true; 289 | } 290 | } 291 | } else { 292 | let lastSample = this._audioSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts); 293 | if (lastSample != null) { 294 | let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration)); 295 | if (distance <= 3) { 296 | distance = 0; 297 | } 298 | let expectedDts = lastSample.dts + lastSample.duration + distance; 299 | dtsCorrection = firstSampleOriginalDts - expectedDts; 300 | } else { // lastSample == null, cannot found 301 | dtsCorrection = 0; 302 | } 303 | } 304 | } 305 | 306 | if (insertPrefixSilentFrame) { 307 | // align audio segment beginDts to match with current video segment's beginDts 308 | let firstSampleDts = firstSampleOriginalDts - dtsCorrection; 309 | let videoSegment = this._videoSegmentInfoList.getLastSegmentBefore(firstSampleOriginalDts); 310 | if (videoSegment != null && videoSegment.beginDts < firstSampleDts) { 311 | let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); 312 | if (silentUnit) { 313 | let dts = videoSegment.beginDts; 314 | let silentFrameDuration = firstSampleDts - videoSegment.beginDts; 315 | Log.v(this.TAG, `InsertPrefixSilentAudio: dts: ${dts}, duration: ${silentFrameDuration}`); 316 | samples.unshift({ unit: silentUnit, dts: dts, pts: dts }); 317 | mdatBytes += silentUnit.byteLength; 318 | } // silentUnit == null: Cannot generate, skip 319 | } else { 320 | insertPrefixSilentFrame = false; 321 | } 322 | } 323 | 324 | let mp4Samples = []; 325 | 326 | // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples 327 | for (let i = 0; i < samples.length; i++) { 328 | let sample = samples[i]; 329 | let unit = sample.unit; 330 | let originalDts = sample.dts - this._dtsBase; 331 | let dts = originalDts; 332 | let needFillSilentFrames = false; 333 | let silentFrames = null; 334 | let sampleDuration = 0; 335 | 336 | if (originalDts < -0.001) { 337 | continue; //pass the first sample with the invalid dts 338 | } 339 | 340 | if (this._audioMeta.codec !== 'mp3') { 341 | // for AAC codec, we need to keep dts increase based on refSampleDuration 342 | let curRefDts = originalDts; 343 | const maxAudioFramesDrift = 3; 344 | if (this._audioNextDts) { 345 | curRefDts = this._audioNextDts; 346 | } 347 | 348 | dtsCorrection = originalDts - curRefDts; 349 | if (dtsCorrection <= -maxAudioFramesDrift * refSampleDuration) { 350 | // If we're overlapping by more than maxAudioFramesDrift number of frame, drop this sample 351 | Log.w(this.TAG, `Dropping 1 audio frame (originalDts: ${originalDts} ms ,curRefDts: ${curRefDts} ms) due to dtsCorrection: ${dtsCorrection} ms overlap.`); 352 | continue; 353 | } 354 | else if (dtsCorrection >= maxAudioFramesDrift * refSampleDuration && this._fillAudioTimestampGap && !Browser.safari) { 355 | // Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap 356 | needFillSilentFrames = true; 357 | // We need to insert silent frames to fill timestamp gap 358 | let frameCount = Math.floor(dtsCorrection / refSampleDuration); 359 | Log.w(this.TAG, 'Large audio timestamp gap detected, may cause AV sync to drift. ' + 360 | 'Silent frames will be generated to avoid unsync.\n' + 361 | `originalDts: ${originalDts} ms, curRefDts: ${curRefDts} ms, ` + 362 | `dtsCorrection: ${Math.round(dtsCorrection)} ms, generate: ${frameCount} frames`); 363 | 364 | 365 | dts = Math.floor(curRefDts); 366 | sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts; 367 | 368 | let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount); 369 | if (silentUnit == null) { 370 | Log.w(this.TAG, 'Unable to generate silent frame for ' + 371 | `${this._audioMeta.originalCodec} with ${this._audioMeta.channelCount} channels, repeat last frame`); 372 | // Repeat last frame 373 | silentUnit = unit; 374 | } 375 | silentFrames = []; 376 | 377 | for (let j = 0; j < frameCount; j++) { 378 | curRefDts = curRefDts + refSampleDuration; 379 | let intDts = Math.floor(curRefDts); // change to integer 380 | let intDuration = Math.floor(curRefDts + refSampleDuration) - intDts; 381 | let frame = { 382 | dts: intDts, 383 | pts: intDts, 384 | cts: 0, 385 | unit: silentUnit, 386 | size: silentUnit.byteLength, 387 | duration: intDuration, // wait for next sample 388 | originalDts: originalDts, 389 | flags: { 390 | isLeading: 0, 391 | dependsOn: 1, 392 | isDependedOn: 0, 393 | hasRedundancy: 0 394 | } 395 | }; 396 | silentFrames.push(frame); 397 | mdatBytes += frame.size; 398 | 399 | } 400 | 401 | this._audioNextDts = curRefDts + refSampleDuration; 402 | 403 | } else { 404 | 405 | dts = Math.floor(curRefDts); 406 | sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts; 407 | this._audioNextDts = curRefDts + refSampleDuration; 408 | 409 | } 410 | } else { 411 | // keep the original dts calculate algorithm for mp3 412 | dts = originalDts - dtsCorrection; 413 | 414 | 415 | if (i !== samples.length - 1) { 416 | let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; 417 | sampleDuration = nextDts - dts; 418 | } else { // the last sample 419 | if (lastSample != null) { // use stashed sample's dts to calculate sample duration 420 | let nextDts = lastSample.dts - this._dtsBase - dtsCorrection; 421 | sampleDuration = nextDts - dts; 422 | } else if (mp4Samples.length >= 1) { // use second last sample duration 423 | sampleDuration = mp4Samples[mp4Samples.length - 1].duration; 424 | } else { // the only one sample, use reference sample duration 425 | sampleDuration = Math.floor(refSampleDuration); 426 | } 427 | } 428 | this._audioNextDts = dts + sampleDuration; 429 | } 430 | 431 | if (firstDts === -1) { 432 | firstDts = dts; 433 | } 434 | mp4Samples.push({ 435 | dts: dts, 436 | pts: dts, 437 | cts: 0, 438 | unit: sample.unit, 439 | size: sample.unit.byteLength, 440 | duration: sampleDuration, 441 | originalDts: originalDts, 442 | flags: { 443 | isLeading: 0, 444 | dependsOn: 1, 445 | isDependedOn: 0, 446 | hasRedundancy: 0 447 | } 448 | }); 449 | 450 | if (needFillSilentFrames) { 451 | // Silent frames should be inserted after wrong-duration frame 452 | mp4Samples.push.apply(mp4Samples, silentFrames); 453 | } 454 | } 455 | 456 | if (mp4Samples.length === 0) { 457 | //no samples need to remux 458 | track.samples = []; 459 | track.length = 0; 460 | Log.w(this.TAG, "no mp4Samples = 0"); 461 | return; 462 | } 463 | 464 | // allocate mdatbox 465 | if (mpegRawTrack) { 466 | // allocate for raw mpeg buffer 467 | mdatbox = new Uint8Array(mdatBytes); 468 | } else { 469 | // allocate for fmp4 mdat box 470 | mdatbox = new Uint8Array(mdatBytes); 471 | // size field 472 | mdatbox[0] = (mdatBytes >>> 24) & 0xFF; 473 | mdatbox[1] = (mdatBytes >>> 16) & 0xFF; 474 | mdatbox[2] = (mdatBytes >>> 8) & 0xFF; 475 | mdatbox[3] = (mdatBytes) & 0xFF; 476 | // type field (fourCC) 477 | mdatbox.set(MP4.types.mdat, 4); 478 | } 479 | 480 | // Write samples into mdatbox 481 | for (let i = 0; i < mp4Samples.length; i++) { 482 | let unit = mp4Samples[i].unit; 483 | mdatbox.set(unit, offset); 484 | offset += unit.byteLength; 485 | } 486 | 487 | let latest = mp4Samples[mp4Samples.length - 1]; 488 | lastDts = latest.dts + latest.duration; 489 | //this._audioNextDts = lastDts; 490 | 491 | // fill media segment info & add to info list 492 | let info = new MediaSegmentInfo(); 493 | info.beginDts = firstDts; 494 | info.endDts = lastDts; 495 | info.beginPts = firstDts; 496 | info.endPts = lastDts; 497 | info.originalBeginDts = mp4Samples[0].originalDts; 498 | info.originalEndDts = latest.originalDts + latest.duration; 499 | info.firstSample = new SampleInfo(mp4Samples[0].dts, 500 | mp4Samples[0].pts, 501 | mp4Samples[0].duration, 502 | mp4Samples[0].originalDts, 503 | false); 504 | info.lastSample = new SampleInfo(latest.dts, 505 | latest.pts, 506 | latest.duration, 507 | latest.originalDts, 508 | false); 509 | if (!this._isLive) { 510 | this._audioSegmentInfoList.append(info); 511 | } 512 | 513 | track.samples = mp4Samples; 514 | track.sequenceNumber++; 515 | 516 | let moofbox; 517 | 518 | if (mpegRawTrack) { 519 | // Generate empty buffer, because useless for raw mpeg 520 | moofbox = new Uint8Array(0); 521 | } else { 522 | // Generate moof for fmp4 segment 523 | moofbox = MP4.moof(track, firstDts); 524 | } 525 | 526 | track.samples = []; 527 | track.length = 0; 528 | 529 | let segment = { 530 | type: 'audio', 531 | data: this._mergeBoxes(moofbox, mdatbox).buffer, 532 | sampleCount: mp4Samples.length, 533 | info: info 534 | }; 535 | 536 | if (mpegRawTrack && firstSegmentAfterSeek) { 537 | // For MPEG audio stream in MSE, if seeking occurred, before appending new buffer 538 | // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. 539 | segment.timestampOffset = firstDts; 540 | } 541 | 542 | Log.i(this.TAG, "send onMediaSegment audio"); 543 | this._onMediaSegment('audio', segment); 544 | } 545 | 546 | _remuxVideo(videoTrack, force) { 547 | Log.i(this.TAG, "_remuxVideo"); 548 | if (this._videoMeta == null) { 549 | return; 550 | } 551 | 552 | let track = videoTrack; 553 | let samples = track.samples; 554 | let dtsCorrection = undefined; 555 | let firstDts = -1, lastDts = -1; 556 | let firstPts = -1, lastPts = -1; 557 | 558 | if (!samples || samples.length === 0) { 559 | Log.w(this.TAG, "no samples"); 560 | return; 561 | } 562 | if (samples.length === 1 && !force) { 563 | // If [sample count in current batch] === 1 && (force != true) 564 | // Ignore and keep in demuxer's queue 565 | Log.w(this.TAG, "no sampes = 1"); 566 | return; 567 | } // else if (force === true) do remux 568 | 569 | let offset = 8; 570 | let mdatbox = null; 571 | let mdatBytes = 8 + videoTrack.length; 572 | 573 | 574 | let lastSample = null; 575 | 576 | // Pop the lastSample and waiting for stash 577 | if (samples.length > 1) { 578 | lastSample = samples.pop(); 579 | mdatBytes -= lastSample.length; 580 | } 581 | 582 | // Insert [stashed lastSample in the previous batch] to the front 583 | if (this._videoStashedLastSample != null) { 584 | let sample = this._videoStashedLastSample; 585 | this._videoStashedLastSample = null; 586 | samples.unshift(sample); 587 | mdatBytes += sample.length; 588 | } 589 | 590 | // Stash the lastSample of current batch, waiting for next batch 591 | if (lastSample != null) { 592 | this._videoStashedLastSample = lastSample; 593 | } 594 | 595 | 596 | let firstSampleOriginalDts = samples[0].dts - this._dtsBase; 597 | 598 | // calculate dtsCorrection 599 | if (this._videoNextDts) { 600 | dtsCorrection = firstSampleOriginalDts - this._videoNextDts; 601 | } else { // this._videoNextDts == undefined 602 | if (this._videoSegmentInfoList.isEmpty()) { 603 | dtsCorrection = 0; 604 | } else { 605 | let lastSample = this._videoSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts); 606 | if (lastSample != null) { 607 | let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration)); 608 | if (distance <= 3) { 609 | distance = 0; 610 | } 611 | let expectedDts = lastSample.dts + lastSample.duration + distance; 612 | dtsCorrection = firstSampleOriginalDts - expectedDts; 613 | } else { // lastSample == null, cannot found 614 | dtsCorrection = 0; 615 | } 616 | } 617 | } 618 | 619 | let info = new MediaSegmentInfo(); 620 | let mp4Samples = []; 621 | 622 | // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples 623 | for (let i = 0; i < samples.length; i++) { 624 | let sample = samples[i]; 625 | let originalDts = sample.dts - this._dtsBase; 626 | let isKeyframe = sample.isKeyframe; 627 | let dts = originalDts - dtsCorrection; 628 | let cts = sample.cts; 629 | let pts = dts + cts; 630 | 631 | if (firstDts === -1) { 632 | firstDts = dts; 633 | firstPts = pts; 634 | } 635 | 636 | let sampleDuration = 0; 637 | 638 | if (i !== samples.length - 1) { 639 | let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection; 640 | sampleDuration = nextDts - dts; 641 | } else { // the last sample 642 | if (lastSample != null) { // use stashed sample's dts to calculate sample duration 643 | let nextDts = lastSample.dts - this._dtsBase - dtsCorrection; 644 | sampleDuration = nextDts - dts; 645 | } else if (mp4Samples.length >= 1) { // use second last sample duration 646 | sampleDuration = mp4Samples[mp4Samples.length - 1].duration; 647 | } else { // the only one sample, use reference sample duration 648 | sampleDuration = Math.floor(this._videoMeta.refSampleDuration); 649 | } 650 | } 651 | 652 | if (isKeyframe) { 653 | let syncPoint = new SampleInfo(dts, pts, sampleDuration, sample.dts, true); 654 | syncPoint.fileposition = sample.fileposition; 655 | info.appendSyncPoint(syncPoint); 656 | } 657 | 658 | mp4Samples.push({ 659 | dts: dts, 660 | pts: pts, 661 | cts: cts, 662 | units: sample.units, 663 | size: sample.length, 664 | isKeyframe: isKeyframe, 665 | duration: sampleDuration, 666 | originalDts: originalDts, 667 | flags: { 668 | isLeading: 0, 669 | dependsOn: isKeyframe ? 2 : 1, 670 | isDependedOn: isKeyframe ? 1 : 0, 671 | hasRedundancy: 0, 672 | isNonSync: isKeyframe ? 0 : 1 673 | } 674 | }); 675 | } 676 | 677 | // allocate mdatbox 678 | mdatbox = new Uint8Array(mdatBytes); 679 | mdatbox[0] = (mdatBytes >>> 24) & 0xFF; 680 | mdatbox[1] = (mdatBytes >>> 16) & 0xFF; 681 | mdatbox[2] = (mdatBytes >>> 8) & 0xFF; 682 | mdatbox[3] = (mdatBytes) & 0xFF; 683 | mdatbox.set(MP4.types.mdat, 4); 684 | 685 | // Write samples into mdatbox 686 | for (let i = 0; i < mp4Samples.length; i++) { 687 | let units = mp4Samples[i].units; 688 | while (units.length) { 689 | let unit = units.shift(); 690 | let data = unit.data; 691 | mdatbox.set(data, offset); 692 | offset += data.byteLength; 693 | } 694 | } 695 | 696 | let latest = mp4Samples[mp4Samples.length - 1]; 697 | lastDts = latest.dts + latest.duration; 698 | lastPts = latest.pts + latest.duration; 699 | this._videoNextDts = lastDts; 700 | 701 | // fill media segment info & add to info list 702 | info.beginDts = firstDts; 703 | info.endDts = lastDts; 704 | info.beginPts = firstPts; 705 | info.endPts = lastPts; 706 | info.originalBeginDts = mp4Samples[0].originalDts; 707 | info.originalEndDts = latest.originalDts + latest.duration; 708 | info.firstSample = new SampleInfo(mp4Samples[0].dts, 709 | mp4Samples[0].pts, 710 | mp4Samples[0].duration, 711 | mp4Samples[0].originalDts, 712 | mp4Samples[0].isKeyframe); 713 | info.lastSample = new SampleInfo(latest.dts, 714 | latest.pts, 715 | latest.duration, 716 | latest.originalDts, 717 | latest.isKeyframe); 718 | if (!this._isLive) { 719 | this._videoSegmentInfoList.append(info); 720 | } 721 | 722 | track.samples = mp4Samples; 723 | track.sequenceNumber++; 724 | 725 | // workaround for chrome < 50: force first sample as a random access point 726 | // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412 727 | if (this._forceFirstIDR) { 728 | let flags = mp4Samples[0].flags; 729 | flags.dependsOn = 2; 730 | flags.isNonSync = 0; 731 | } 732 | 733 | let moofbox = MP4.moof(track, firstDts); 734 | track.samples = []; 735 | track.length = 0; 736 | 737 | Log.i(this.TAG, "send onMediaSegment video"); 738 | this._onMediaSegment('video', { 739 | type: 'video', 740 | data: this._mergeBoxes(moofbox, mdatbox).buffer, 741 | sampleCount: mp4Samples.length, 742 | info: info 743 | }); 744 | } 745 | 746 | _mergeBoxes(moof, mdat) { 747 | let result = new Uint8Array(moof.byteLength + mdat.byteLength); 748 | result.set(moof, 0); 749 | result.set(mdat, moof.byteLength); 750 | return result; 751 | } 752 | 753 | } 754 | 755 | export default MP4Remuxer; 756 | -------------------------------------------------------------------------------- /src/formats/mp4.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * This file is derived from dailymotion's hls.js library (hls.js/src/remux/mp4-generator.js) 5 | * @author zheng qian 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // MP4 boxes generator for ISO BMFF (ISO Base Media File Format, defined in ISO/IEC 14496-12) 21 | class MP4 { 22 | static init() { 23 | MP4.types = { 24 | avc1: [], avcC: [], btrt: [], dinf: [], 25 | dref: [], esds: [], ftyp: [], hdlr: [], 26 | mdat: [], mdhd: [], mdia: [], mfhd: [], 27 | minf: [], moof: [], moov: [], mp4a: [], 28 | mvex: [], mvhd: [], sdtp: [], stbl: [], 29 | stco: [], stsc: [], stsd: [], stsz: [], 30 | stts: [], tfdt: [], tfhd: [], traf: [], 31 | trak: [], trun: [], trex: [], tkhd: [], 32 | vmhd: [], smhd: [], '.mp3': [] 33 | }; 34 | 35 | for (let name in MP4.types) { 36 | if (MP4.types.hasOwnProperty(name)) { 37 | MP4.types[name] = [ 38 | name.charCodeAt(0), 39 | name.charCodeAt(1), 40 | name.charCodeAt(2), 41 | name.charCodeAt(3) 42 | ]; 43 | } 44 | } 45 | 46 | let constants = MP4.constants = {}; 47 | 48 | constants.FTYP = new Uint8Array([ 49 | 0x69, 0x73, 0x6F, 0x6D, // major_brand: isom 50 | 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01 51 | 0x69, 0x73, 0x6F, 0x6D, // isom 52 | 0x61, 0x76, 0x63, 0x31 // avc1 53 | ]); 54 | 55 | constants.STSD_PREFIX = new Uint8Array([ 56 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 57 | 0x00, 0x00, 0x00, 0x01 // entry_count 58 | ]); 59 | 60 | constants.STTS = new Uint8Array([ 61 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 62 | 0x00, 0x00, 0x00, 0x00 // entry_count 63 | ]); 64 | 65 | constants.STSC = constants.STCO = constants.STTS; 66 | 67 | constants.STSZ = new Uint8Array([ 68 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 69 | 0x00, 0x00, 0x00, 0x00, // sample_size 70 | 0x00, 0x00, 0x00, 0x00 // sample_count 71 | ]); 72 | 73 | constants.HDLR_VIDEO = new Uint8Array([ 74 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 75 | 0x00, 0x00, 0x00, 0x00, // pre_defined 76 | 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 77 | 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes 78 | 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 80 | 0x56, 0x69, 0x64, 0x65, 81 | 0x6F, 0x48, 0x61, 0x6E, 82 | 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler 83 | ]); 84 | 85 | constants.HDLR_AUDIO = new Uint8Array([ 86 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 87 | 0x00, 0x00, 0x00, 0x00, // pre_defined 88 | 0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun' 89 | 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes 90 | 0x00, 0x00, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x00, 92 | 0x53, 0x6F, 0x75, 0x6E, 93 | 0x64, 0x48, 0x61, 0x6E, 94 | 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler 95 | ]); 96 | 97 | constants.DREF = new Uint8Array([ 98 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 99 | 0x00, 0x00, 0x00, 0x01, // entry_count 100 | 0x00, 0x00, 0x00, 0x0C, // entry_size 101 | 0x75, 0x72, 0x6C, 0x20, // type 'url ' 102 | 0x00, 0x00, 0x00, 0x01 // version(0) + flags 103 | ]); 104 | 105 | // Sound media header 106 | constants.SMHD = new Uint8Array([ 107 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 108 | 0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2) 109 | ]); 110 | 111 | // video media header 112 | constants.VMHD = new Uint8Array([ 113 | 0x00, 0x00, 0x00, 0x01, // version(0) + flags 114 | 0x00, 0x00, // graphicsmode: 2 bytes 115 | 0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes 116 | 0x00, 0x00 117 | ]); 118 | } 119 | 120 | // Generate a box 121 | static box(type) { 122 | let size = 8; 123 | let result; 124 | let datas = Array.prototype.slice.call(arguments, 1); 125 | let arrayCount = datas.length; 126 | 127 | for (let i = 0; i < arrayCount; i++) { 128 | size += datas[i].byteLength; 129 | } 130 | 131 | result = new Uint8Array(size); 132 | result[0] = (size >>> 24) & 0xFF; // size 133 | result[1] = (size >>> 16) & 0xFF; 134 | result[2] = (size >>> 8) & 0xFF; 135 | result[3] = (size) & 0xFF; 136 | 137 | result.set(type, 4); // type 138 | 139 | let offset = 8; 140 | for (let i = 0; i < arrayCount; i++) { // data body 141 | result.set(datas[i], offset); 142 | offset += datas[i].byteLength; 143 | } 144 | 145 | return result; 146 | } 147 | 148 | // emit ftyp & moov 149 | static generateInitSegment(meta) { 150 | let ftyp = MP4.box(MP4.types.ftyp, MP4.constants.FTYP); 151 | let moov = MP4.moov(meta); 152 | 153 | let result = new Uint8Array(ftyp.byteLength + moov.byteLength); 154 | result.set(ftyp, 0); 155 | result.set(moov, ftyp.byteLength); 156 | return result; 157 | } 158 | 159 | // Movie metadata box 160 | static moov(meta) { 161 | let mvhd = MP4.mvhd(meta.timescale, meta.duration); 162 | let trak = MP4.trak(meta); 163 | let mvex = MP4.mvex(meta); 164 | return MP4.box(MP4.types.moov, mvhd, trak, mvex); 165 | } 166 | 167 | // Movie header box 168 | static mvhd(timescale, duration) { 169 | return MP4.box(MP4.types.mvhd, new Uint8Array([ 170 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 171 | 0x00, 0x00, 0x00, 0x00, // creation_time 172 | 0x00, 0x00, 0x00, 0x00, // modification_time 173 | (timescale >>> 24) & 0xFF, // timescale: 4 bytes 174 | (timescale >>> 16) & 0xFF, 175 | (timescale >>> 8) & 0xFF, 176 | (timescale) & 0xFF, 177 | (duration >>> 24) & 0xFF, // duration: 4 bytes 178 | (duration >>> 16) & 0xFF, 179 | (duration >>> 8) & 0xFF, 180 | (duration) & 0xFF, 181 | 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 182 | 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes) 183 | 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes 184 | 0x00, 0x00, 0x00, 0x00, 185 | 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- 186 | 0x00, 0x00, 0x00, 0x00, 187 | 0x00, 0x00, 0x00, 0x00, 188 | 0x00, 0x00, 0x00, 0x00, 189 | 0x00, 0x01, 0x00, 0x00, 190 | 0x00, 0x00, 0x00, 0x00, 191 | 0x00, 0x00, 0x00, 0x00, 192 | 0x00, 0x00, 0x00, 0x00, 193 | 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- 194 | 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes---- 195 | 0x00, 0x00, 0x00, 0x00, 196 | 0x00, 0x00, 0x00, 0x00, 197 | 0x00, 0x00, 0x00, 0x00, 198 | 0x00, 0x00, 0x00, 0x00, 199 | 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes---- 200 | 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID 201 | ])); 202 | } 203 | 204 | // Track box 205 | static trak(meta) { 206 | return MP4.box(MP4.types.trak, MP4.tkhd(meta), MP4.mdia(meta)); 207 | } 208 | 209 | // Track header box 210 | static tkhd(meta) { 211 | let trackId = meta.id, duration = meta.duration; 212 | let width = meta.presentWidth, height = meta.presentHeight; 213 | 214 | return MP4.box(MP4.types.tkhd, new Uint8Array([ 215 | 0x00, 0x00, 0x00, 0x07, // version(0) + flags 216 | 0x00, 0x00, 0x00, 0x00, // creation_time 217 | 0x00, 0x00, 0x00, 0x00, // modification_time 218 | (trackId >>> 24) & 0xFF, // track_ID: 4 bytes 219 | (trackId >>> 16) & 0xFF, 220 | (trackId >>> 8) & 0xFF, 221 | (trackId) & 0xFF, 222 | 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes 223 | (duration >>> 24) & 0xFF, // duration: 4 bytes 224 | (duration >>> 16) & 0xFF, 225 | (duration >>> 8) & 0xFF, 226 | (duration) & 0xFF, 227 | 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes 228 | 0x00, 0x00, 0x00, 0x00, 229 | 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes) 230 | 0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes) 231 | 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- 232 | 0x00, 0x00, 0x00, 0x00, 233 | 0x00, 0x00, 0x00, 0x00, 234 | 0x00, 0x00, 0x00, 0x00, 235 | 0x00, 0x01, 0x00, 0x00, 236 | 0x00, 0x00, 0x00, 0x00, 237 | 0x00, 0x00, 0x00, 0x00, 238 | 0x00, 0x00, 0x00, 0x00, 239 | 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- 240 | (width >>> 8) & 0xFF, // width and height 241 | (width) & 0xFF, 242 | 0x00, 0x00, 243 | (height >>> 8) & 0xFF, 244 | (height) & 0xFF, 245 | 0x00, 0x00 246 | ])); 247 | } 248 | 249 | // Media Box 250 | static mdia(meta) { 251 | return MP4.box(MP4.types.mdia, MP4.mdhd(meta), MP4.hdlr(meta), MP4.minf(meta)); 252 | } 253 | 254 | // Media header box 255 | static mdhd(meta) { 256 | let timescale = meta.timescale; 257 | let duration = meta.duration; 258 | return MP4.box(MP4.types.mdhd, new Uint8Array([ 259 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 260 | 0x00, 0x00, 0x00, 0x00, // creation_time 261 | 0x00, 0x00, 0x00, 0x00, // modification_time 262 | (timescale >>> 24) & 0xFF, // timescale: 4 bytes 263 | (timescale >>> 16) & 0xFF, 264 | (timescale >>> 8) & 0xFF, 265 | (timescale) & 0xFF, 266 | (duration >>> 24) & 0xFF, // duration: 4 bytes 267 | (duration >>> 16) & 0xFF, 268 | (duration >>> 8) & 0xFF, 269 | (duration) & 0xFF, 270 | 0x55, 0xC4, // language: und (undetermined) 271 | 0x00, 0x00 // pre_defined = 0 272 | ])); 273 | } 274 | 275 | // Media handler reference box 276 | static hdlr(meta) { 277 | let data; 278 | if (meta.type === 'audio') { 279 | data = MP4.constants.HDLR_AUDIO; 280 | } else { 281 | data = MP4.constants.HDLR_VIDEO; 282 | } 283 | return MP4.box(MP4.types.hdlr, data); 284 | } 285 | 286 | // Media infomation box 287 | static minf(meta) { 288 | let xmhd; 289 | if (meta.type === 'audio') { 290 | xmhd = MP4.box(MP4.types.smhd, MP4.constants.SMHD); 291 | } else { 292 | xmhd = MP4.box(MP4.types.vmhd, MP4.constants.VMHD); 293 | } 294 | return MP4.box(MP4.types.minf, xmhd, MP4.dinf(), MP4.stbl(meta)); 295 | } 296 | 297 | // Data infomation box 298 | static dinf() { 299 | return MP4.box(MP4.types.dinf, 300 | MP4.box(MP4.types.dref, MP4.constants.DREF) 301 | ); 302 | } 303 | 304 | // Sample table box 305 | static stbl(meta) { 306 | return MP4.box(MP4.types.stbl, // type: stbl 307 | MP4.stsd(meta), // Sample Description Table 308 | MP4.box(MP4.types.stts, MP4.constants.STTS), // Time-To-Sample 309 | MP4.box(MP4.types.stsc, MP4.constants.STSC), // Sample-To-Chunk 310 | MP4.box(MP4.types.stsz, MP4.constants.STSZ), // Sample size 311 | MP4.box(MP4.types.stco, MP4.constants.STCO) // Chunk offset 312 | ); 313 | } 314 | 315 | // Sample description box 316 | static stsd(meta) { 317 | if (meta.type === 'audio') { 318 | if (meta.codec === 'mp3') { 319 | return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp3(meta)); 320 | } 321 | // else: aac -> mp4a 322 | return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp4a(meta)); 323 | } else { 324 | return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.avc1(meta)); 325 | } 326 | } 327 | 328 | static mp3(meta) { 329 | let channelCount = meta.channelCount; 330 | let sampleRate = meta.audioSampleRate; 331 | 332 | let data = new Uint8Array([ 333 | 0x00, 0x00, 0x00, 0x00, // reserved(4) 334 | 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) 335 | 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes 336 | 0x00, 0x00, 0x00, 0x00, 337 | 0x00, channelCount, // channelCount(2) 338 | 0x00, 0x10, // sampleSize(2) 339 | 0x00, 0x00, 0x00, 0x00, // reserved(4) 340 | (sampleRate >>> 8) & 0xFF, // Audio sample rate 341 | (sampleRate) & 0xFF, 342 | 0x00, 0x00 343 | ]); 344 | 345 | return MP4.box(MP4.types['.mp3'], data); 346 | } 347 | 348 | static mp4a(meta) { 349 | let channelCount = meta.channelCount; 350 | let sampleRate = meta.audioSampleRate; 351 | 352 | let data = new Uint8Array([ 353 | 0x00, 0x00, 0x00, 0x00, // reserved(4) 354 | 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) 355 | 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes 356 | 0x00, 0x00, 0x00, 0x00, 357 | 0x00, channelCount, // channelCount(2) 358 | 0x00, 0x10, // sampleSize(2) 359 | 0x00, 0x00, 0x00, 0x00, // reserved(4) 360 | (sampleRate >>> 8) & 0xFF, // Audio sample rate 361 | (sampleRate) & 0xFF, 362 | 0x00, 0x00 363 | ]); 364 | 365 | return MP4.box(MP4.types.mp4a, data, MP4.esds(meta)); 366 | } 367 | 368 | static esds(meta) { 369 | let config = meta.config || []; 370 | let configSize = config.length; 371 | let data = new Uint8Array([ 372 | 0x00, 0x00, 0x00, 0x00, // version 0 + flags 373 | 374 | 0x03, // descriptor_type 375 | 0x17 + configSize, // length3 376 | 0x00, 0x01, // es_id 377 | 0x00, // stream_priority 378 | 379 | 0x04, // descriptor_type 380 | 0x0F + configSize, // length 381 | 0x40, // codec: mpeg4_audio 382 | 0x15, // stream_type: Audio 383 | 0x00, 0x00, 0x00, // buffer_size 384 | 0x00, 0x00, 0x00, 0x00, // maxBitrate 385 | 0x00, 0x00, 0x00, 0x00, // avgBitrate 386 | 387 | 0x05 // descriptor_type 388 | ].concat([ 389 | configSize 390 | ]).concat( 391 | config 392 | ).concat([ 393 | 0x06, 0x01, 0x02 // GASpecificConfig 394 | ])); 395 | return MP4.box(MP4.types.esds, data); 396 | } 397 | 398 | static avc1(meta) { 399 | let avcc = meta.avcc; 400 | let width = meta.codecWidth, height = meta.codecHeight; 401 | 402 | let data = new Uint8Array([ 403 | 0x00, 0x00, 0x00, 0x00, // reserved(4) 404 | 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) 405 | 0x00, 0x00, 0x00, 0x00, // pre_defined(2) + reserved(2) 406 | 0x00, 0x00, 0x00, 0x00, // pre_defined: 3 * 4 bytes 407 | 0x00, 0x00, 0x00, 0x00, 408 | 0x00, 0x00, 0x00, 0x00, 409 | (width >>> 8) & 0xFF, // width: 2 bytes 410 | (width) & 0xFF, 411 | (height >>> 8) & 0xFF, // height: 2 bytes 412 | (height) & 0xFF, 413 | 0x00, 0x48, 0x00, 0x00, // horizresolution: 4 bytes 414 | 0x00, 0x48, 0x00, 0x00, // vertresolution: 4 bytes 415 | 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes 416 | 0x00, 0x01, // frame_count 417 | 0x0A, // strlen 418 | 0x78, 0x71, 0x71, 0x2F, // compressorname: 32 bytes 419 | 0x66, 0x6C, 0x76, 0x2E, 420 | 0x6A, 0x73, 0x00, 0x00, 421 | 0x00, 0x00, 0x00, 0x00, 422 | 0x00, 0x00, 0x00, 0x00, 423 | 0x00, 0x00, 0x00, 0x00, 424 | 0x00, 0x00, 0x00, 0x00, 425 | 0x00, 0x00, 0x00, 426 | 0x00, 0x18, // depth 427 | 0xFF, 0xFF // pre_defined = -1 428 | ]); 429 | return MP4.box(MP4.types.avc1, data, MP4.box(MP4.types.avcC, avcc)); 430 | } 431 | 432 | // Movie Extends box 433 | static mvex(meta) { 434 | return MP4.box(MP4.types.mvex, MP4.trex(meta)); 435 | } 436 | 437 | // Track Extends box 438 | static trex(meta) { 439 | let trackId = meta.id; 440 | let data = new Uint8Array([ 441 | 0x00, 0x00, 0x00, 0x00, // version(0) + flags 442 | (trackId >>> 24) & 0xFF, // track_ID 443 | (trackId >>> 16) & 0xFF, 444 | (trackId >>> 8) & 0xFF, 445 | (trackId) & 0xFF, 446 | 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 447 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration 448 | 0x00, 0x00, 0x00, 0x00, // default_sample_size 449 | 0x00, 0x01, 0x00, 0x01 // default_sample_flags 450 | ]); 451 | return MP4.box(MP4.types.trex, data); 452 | } 453 | 454 | // Movie fragment box 455 | static moof(track, baseMediaDecodeTime) { 456 | return MP4.box(MP4.types.moof, MP4.mfhd(track.sequenceNumber), MP4.traf(track, baseMediaDecodeTime)); 457 | } 458 | 459 | static mfhd(sequenceNumber) { 460 | let data = new Uint8Array([ 461 | 0x00, 0x00, 0x00, 0x00, 462 | (sequenceNumber >>> 24) & 0xFF, // sequence_number: int32 463 | (sequenceNumber >>> 16) & 0xFF, 464 | (sequenceNumber >>> 8) & 0xFF, 465 | (sequenceNumber) & 0xFF 466 | ]); 467 | return MP4.box(MP4.types.mfhd, data); 468 | } 469 | 470 | // Track fragment box 471 | static traf(track, baseMediaDecodeTime) { 472 | let trackId = track.id; 473 | 474 | // Track fragment header box 475 | let tfhd = MP4.box(MP4.types.tfhd, new Uint8Array([ 476 | 0x00, 0x00, 0x00, 0x00, // version(0) & flags 477 | (trackId >>> 24) & 0xFF, // track_ID 478 | (trackId >>> 16) & 0xFF, 479 | (trackId >>> 8) & 0xFF, 480 | (trackId) & 0xFF 481 | ])); 482 | // Track Fragment Decode Time 483 | let tfdt = MP4.box(MP4.types.tfdt, new Uint8Array([ 484 | 0x00, 0x00, 0x00, 0x00, // version(0) & flags 485 | (baseMediaDecodeTime >>> 24) & 0xFF, // baseMediaDecodeTime: int32 486 | (baseMediaDecodeTime >>> 16) & 0xFF, 487 | (baseMediaDecodeTime >>> 8) & 0xFF, 488 | (baseMediaDecodeTime) & 0xFF 489 | ])); 490 | let sdtp = MP4.sdtp(track); 491 | let trun = MP4.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8); 492 | 493 | return MP4.box(MP4.types.traf, tfhd, tfdt, trun, sdtp); 494 | } 495 | 496 | // Sample Dependency Type box 497 | static sdtp(track) { 498 | let samples = track.samples || []; 499 | let sampleCount = samples.length; 500 | let data = new Uint8Array(4 + sampleCount); 501 | // 0~4 bytes: version(0) & flags 502 | for (let i = 0; i < sampleCount; i++) { 503 | let flags = samples[i].flags; 504 | data[i + 4] = (flags.isLeading << 6) // is_leading: 2 (bit) 505 | | (flags.dependsOn << 4) // sample_depends_on 506 | | (flags.isDependedOn << 2) // sample_is_depended_on 507 | | (flags.hasRedundancy); // sample_has_redundancy 508 | } 509 | return MP4.box(MP4.types.sdtp, data); 510 | } 511 | 512 | // Track fragment run box 513 | static trun(track, offset) { 514 | let samples = track.samples || []; 515 | let sampleCount = samples.length; 516 | let dataSize = 12 + 16 * sampleCount; 517 | let data = new Uint8Array(dataSize); 518 | offset += 8 + dataSize; 519 | 520 | data.set([ 521 | 0x00, 0x00, 0x0F, 0x01, // version(0) & flags 522 | (sampleCount >>> 24) & 0xFF, // sample_count 523 | (sampleCount >>> 16) & 0xFF, 524 | (sampleCount >>> 8) & 0xFF, 525 | (sampleCount) & 0xFF, 526 | (offset >>> 24) & 0xFF, // data_offset 527 | (offset >>> 16) & 0xFF, 528 | (offset >>> 8) & 0xFF, 529 | (offset) & 0xFF 530 | ], 0); 531 | 532 | for (let i = 0; i < sampleCount; i++) { 533 | let duration = samples[i].duration; 534 | let size = samples[i].size; 535 | let flags = samples[i].flags; 536 | let cts = samples[i].cts; 537 | data.set([ 538 | (duration >>> 24) & 0xFF, // sample_duration 539 | (duration >>> 16) & 0xFF, 540 | (duration >>> 8) & 0xFF, 541 | (duration) & 0xFF, 542 | (size >>> 24) & 0xFF, // sample_size 543 | (size >>> 16) & 0xFF, 544 | (size >>> 8) & 0xFF, 545 | (size) & 0xFF, 546 | (flags.isLeading << 2) | flags.dependsOn, // sample_flags 547 | (flags.isDependedOn << 6) | (flags.hasRedundancy << 4) | flags.isNonSync, 548 | 0x00, 0x00, // sample_degradation_priority 549 | (cts >>> 24) & 0xFF, // sample_composition_time_offset 550 | (cts >>> 16) & 0xFF, 551 | (cts >>> 8) & 0xFF, 552 | (cts) & 0xFF 553 | ], 12 + 16 * i); 554 | } 555 | return MP4.box(MP4.types.trun, data); 556 | } 557 | 558 | static mdat(data) { 559 | return MP4.box(MP4.types.mdat, data); 560 | } 561 | 562 | } 563 | 564 | MP4.init(); 565 | 566 | export default MP4; 567 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | 22 | import { WebRTMP } from "./webrtmp"; 23 | import Log from "./utils/logger"; 24 | export { WebRTMP } from "./webrtmp"; 25 | 26 | export function createWebRTMP(){ 27 | return new WebRTMP(); 28 | } 29 | 30 | window["Log"] = Log; 31 | -------------------------------------------------------------------------------- /src/rtmp/AMF0Object.js: -------------------------------------------------------------------------------- 1 | import {_byteArrayToNumber, _byteArrayToString, _numberToByteArray, _stringToByteArray} from "../utils/utils"; 2 | import Log from "../utils/logger"; 3 | 4 | class AMF0Object { 5 | TAG = "AMF0Object"; 6 | 7 | data; 8 | 9 | params; 10 | 11 | /** 12 | * 13 | * @param {Object} params 14 | */ 15 | constructor(params) { 16 | if(params) { 17 | this.params = params; 18 | Log.d(this.TAG, "cmd: " + this.params[0]); 19 | } 20 | } 21 | 22 | /** 23 | * 24 | * @param {Uint8Array} data 25 | * @returns {*[]} 26 | */ 27 | parseAMF0(data) { 28 | this.data = Array.from(data); 29 | let obj = []; 30 | 31 | while (this.data.length > 0) { 32 | const var_type = this.data.shift(); 33 | 34 | switch(var_type) { 35 | case 0x00: // Number 36 | obj.push(_byteArrayToNumber(this.data.slice(0, 8))); 37 | this.data = this.data.slice(8); 38 | break; 39 | 40 | case 0x01: // boolean 41 | if (this.data.shift() === 0) { 42 | obj.push(false); 43 | } else { 44 | obj.push(true); 45 | } 46 | 47 | break; 48 | 49 | case 0x02: // String 50 | let len = (this.data[0] << 8) | (this.data[1]); 51 | this.data = this.data.slice(2); 52 | 53 | obj.push(_byteArrayToString(this.data.slice(0, len))); 54 | this.data = this.data.slice(len); 55 | break; 56 | 57 | case 0x03: // AMF encoded object 58 | obj.push(this._parseAMF0Object()); 59 | break; 60 | 61 | case 0x05: // NUll 62 | obj.push(null); 63 | break; 64 | 65 | default: 66 | Log.w(this.TAG, "var_type: " + var_type + " not yet implemented"); 67 | break; 68 | } 69 | } 70 | this.params = obj; 71 | return obj; 72 | } 73 | 74 | _parseAMF0Object() { 75 | let o2 = {}; 76 | 77 | while (this.data.length > 0) { 78 | let keylen = (this.data[0] << 8) | (this.data[1]); this.data = this.data.slice(2); 79 | 80 | // Object end marker 81 | if (keylen === 0 && this.data[0] === 9) { 82 | this.data = this.data.slice(1); 83 | return o2; 84 | } 85 | 86 | let keyName = _byteArrayToString(this.data.slice(0, keylen)); this.data = this.data.slice(keylen); 87 | 88 | const var_type = this.data.shift(); 89 | 90 | switch(var_type) { 91 | case 0x00: // Number 92 | o2[keyName] = _byteArrayToNumber(this.data.slice(0, 8)); 93 | this.data = this.data.slice(8); 94 | break; 95 | 96 | case 0x01: // boolean 97 | if (this.data.shift() === 0) { 98 | o2[keyName] = false; 99 | } else { 100 | o2[keyName] = true; 101 | } 102 | 103 | break; 104 | 105 | case 0x02: // String 106 | let len = (this.data[0] << 8) | (this.data[1]); 107 | this.data = this.data.slice(2); 108 | 109 | o2[keyName] = _byteArrayToString(this.data.slice(0, len)); 110 | this.data = this.data.slice(len); 111 | break; 112 | 113 | case 0x05: 114 | o2[keyName] = null; 115 | break; 116 | 117 | default: 118 | Log.w(this.TAG, "var_type: " + var_type + " not yet implemented"); 119 | break; 120 | } 121 | } 122 | 123 | return o2; 124 | } 125 | 126 | /** 127 | * 128 | * @returns {Uint8Array} 129 | */ 130 | getBytes() { 131 | let bytes = []; 132 | 133 | for(let i = 0; i < this.params.length; i++) { 134 | const param = this.params[i]; 135 | 136 | switch(typeof param){ 137 | case "string": 138 | // Command 139 | bytes.push(0x02); // String 140 | bytes.push(param.length >>> 8); 141 | bytes.push(param.length); 142 | bytes = bytes.concat(_stringToByteArray(param)); 143 | break; 144 | 145 | case "number": 146 | // TransactionID 147 | bytes.push(0x00); // Number 148 | bytes = bytes.concat(_numberToByteArray(param)); 149 | break; 150 | 151 | case "object": 152 | // Command Object 153 | bytes.push(0x03); // Object 154 | 155 | for (let key in param) { 156 | let value = param[key]; 157 | let keylength = key.length; 158 | 159 | bytes.push(keylength >>> 8); 160 | bytes.push(keylength); 161 | bytes = bytes.concat(_stringToByteArray(key)); 162 | 163 | switch(typeof value) { 164 | case "object": 165 | if (value == null) { 166 | bytes.push(0x05); // Null 167 | } 168 | 169 | break; 170 | 171 | case "string": 172 | const length = value.length; 173 | bytes.push(0x02); 174 | bytes.push(length >>> 8); 175 | bytes.push(length); 176 | bytes = bytes.concat(_stringToByteArray(value)) 177 | break; 178 | 179 | case "number": 180 | bytes.push(0x00); 181 | bytes = bytes.concat(_numberToByteArray(value)) 182 | break; 183 | 184 | case "boolean": 185 | bytes.push(0x01); 186 | if (value) bytes.push(0x01); 187 | else bytes.push(0x00); 188 | break; 189 | 190 | default: 191 | Log.w(this.TAG, typeof value, " not yet implementd"); 192 | break; 193 | } 194 | } 195 | 196 | bytes.push(0x00); // End Marker 197 | bytes.push(0x00); 198 | bytes.push(0x09); 199 | break; 200 | 201 | case "boolean": 202 | bytes.push(0x01); 203 | if(param) bytes.push(0x01); 204 | else bytes.push(0x00); 205 | break; 206 | 207 | default: 208 | Log.w(this.TAG, typeof param, " not yet implementd"); 209 | break; 210 | } 211 | } 212 | 213 | return new Uint8Array(bytes); 214 | } 215 | 216 | getCommand(){ 217 | return this.params[0]; 218 | } 219 | 220 | getTransactionId(){ 221 | return this.params[1]; 222 | } 223 | 224 | getCommandObject(){ 225 | return this.params[2]; 226 | } 227 | 228 | getAdditionalInfo(){ 229 | return this.params[3]; 230 | } 231 | } 232 | 233 | export default AMF0Object; 234 | -------------------------------------------------------------------------------- /src/rtmp/Chunk.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Copyright (C) 2023 itNOX. All Rights Reserved. 5 | * 6 | * @author Michael Balen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * 20 | */ 21 | 22 | import {_concatArrayBuffers} from "../utils/utils"; 23 | import Log from "../utils/logger"; 24 | 25 | class Chunk{ 26 | TAG = "Chunk"; 27 | chunk_stream_id = 0; 28 | 29 | length; 30 | 31 | message_type; 32 | message_stream_id = 0; 33 | 34 | timestamp; 35 | CHUNK_SIZE = 128; 36 | payload; 37 | 38 | /** 39 | * @param {RTMPMessage} message 40 | */ 41 | constructor(message) { // RTMP Message 42 | this.payload = message.getPayload(); 43 | this.length = this.payload.length; 44 | this.message_type = message.getMessageType(); 45 | this.message_stream_id = message.getMessageStreamID(); 46 | } 47 | 48 | /** 49 | * get bytes of the current chunk 50 | * @returns {Uint8Array} 51 | */ 52 | getBytes(){ 53 | let p = new Uint8Array(this.payload); 54 | 55 | let ret = new Uint8Array(0); 56 | let fmt = 0; 57 | 58 | do { 59 | Log.d(this.TAG, "create chunk: " + p.length); 60 | ret = _concatArrayBuffers(ret, this._getHeaderBytes(fmt), p.slice(0,this.CHUNK_SIZE)); 61 | p = p.slice(this.CHUNK_SIZE); 62 | fmt = 0x3; // next chunk without header 63 | 64 | } while(p.length > 0); 65 | 66 | return ret; 67 | } 68 | 69 | /** 70 | * get bytes from chunk header 71 | * @param {Number} fmt 72 | * @returns {Uint8Array} 73 | * @private 74 | */ 75 | _getHeaderBytes(fmt){ 76 | let basic_header; 77 | let header; 78 | 79 | if(this.chunk_stream_id < 63) { 80 | basic_header = new Uint8Array(1); 81 | basic_header[0] = (fmt << 6) | this.chunk_stream_id; 82 | 83 | } else if(this.chunk_stream_id < 65599) { 84 | basic_header = new Uint8Array(2); 85 | basic_header[0] = (fmt << 6); 86 | basic_header[1] = (this.chunk_stream_id -64); 87 | 88 | } else { 89 | basic_header = new Uint8Array(3); 90 | basic_header[0] = (fmt << 6) | 63; 91 | basic_header[1] = ((this.chunk_stream_id -64) >>> 8); 92 | basic_header[2] = ((this.chunk_stream_id -64)); 93 | } 94 | 95 | switch(fmt){ 96 | case 0x0: 97 | header = new Uint8Array(11); 98 | header[0] = (this.timestamp >>> 16); 99 | header[1] = (this.timestamp >>> 8); 100 | header[2] = (this.timestamp); 101 | 102 | header[3] = (this.length >>> 16); 103 | header[4] = (this.length >>> 8); 104 | header[5] = (this.length); 105 | 106 | header[6] = (this.message_type); 107 | 108 | header[7] = (this.message_stream_id >>> 24); 109 | header[8] = (this.message_stream_id >>> 16); 110 | header[9] = (this.message_stream_id >>> 8); 111 | header[10] = (this.message_stream_id); 112 | break; 113 | 114 | case 0x1: 115 | header = new Uint8Array(7); 116 | header[0] = (this.timestamp >>> 16); 117 | header[1] = (this.timestamp >>> 8); 118 | header[2] = (this.timestamp); 119 | 120 | header[3] = (this.length >>> 16); 121 | header[4] = (this.length >>> 8); 122 | header[5] = (this.length); 123 | 124 | header[6] = (this.message_type); 125 | break; 126 | 127 | 128 | case 0x2: 129 | header = new Uint8Array(3); 130 | header[0] = (this.timestamp >>> 16); 131 | header[1] = (this.timestamp >>> 8); 132 | header[2] = (this.timestamp); 133 | break; 134 | 135 | case 0x3: 136 | header = new Uint8Array(0); 137 | break; 138 | } 139 | 140 | return _concatArrayBuffers(basic_header, header); 141 | } 142 | 143 | getPayload(){ 144 | return this.payload; 145 | } 146 | 147 | getMessageType(){ 148 | return this.message_type; 149 | } 150 | 151 | getMessageStreamID() { 152 | return this.message_stream_id; 153 | } 154 | 155 | setChunkSize(size){ 156 | this.CHUNK_SIZE = size; 157 | } 158 | 159 | /** 160 | * Sets the Chunk StreamID 161 | * @param {Number} chunk_stream_id 162 | */ 163 | setChunkStreamID(chunk_stream_id) { 164 | Log.d(this.TAG, "setChunkStreamID:" + chunk_stream_id); 165 | this.chunk_stream_id = chunk_stream_id; 166 | } 167 | 168 | /** 169 | * Sets the Message StreamID of the Chunk 170 | * @param {Number} message_stream_id 171 | */ 172 | setMessageStreamID(message_stream_id) { 173 | this.message_stream_id = message_stream_id; 174 | } 175 | 176 | /** 177 | * Sets the Timestamp of the chunk 178 | * @param {Number} timestamp 179 | */ 180 | setTimestamp(timestamp){ 181 | this.timestamp = timestamp; 182 | } 183 | } 184 | 185 | export default Chunk; 186 | -------------------------------------------------------------------------------- /src/rtmp/ChunkParser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import RTMPMessage from "./RTMPMessage"; 22 | import {_concatArrayBuffers} from "../utils/utils"; 23 | import Log from "../utils/logger"; 24 | 25 | /** 26 | * Class for parsing a Chunk 27 | */ 28 | class ChunkParser { 29 | TAG = "ChunkParser"; 30 | 31 | /** 32 | * 33 | * @type {number} 34 | */ 35 | static CHUNK_SIZE = 128; 36 | _chunkstreams = []; 37 | 38 | /** 39 | * @type {Uint8Array} 40 | */ 41 | _buffer = new Uint8Array(0); 42 | 43 | /** 44 | * 45 | * @param {RTMPMessageHandler} conn_worker 46 | */ 47 | constructor(conn_worker) { 48 | this.conn_worker = conn_worker; 49 | } 50 | 51 | /** 52 | * Parse chunk data. Just simply add your UInt8Array, splitting and concating is automatically 53 | * @param {Uint8Array} newdata 54 | */ 55 | parseChunk(newdata){ 56 | let msg; 57 | let timestamp; 58 | let fmt; 59 | 60 | this._buffer = _concatArrayBuffers(this._buffer, newdata); // Neues Packet an Buffer anfügen 61 | 62 | do { 63 | Log.d(this.TAG, "buffer length: " + this._buffer.length); 64 | 65 | if(this._buffer.length < 100) Log.d(this.TAG, this._buffer); 66 | 67 | /** 68 | * 69 | * @type {Uint8Array} 70 | */ 71 | let data = this._buffer; 72 | let header_length = 0; 73 | let message_length = 0; 74 | let payload_length = 0; 75 | 76 | // Message Header Type 77 | fmt = ((data[0] & 0xC0) >>> 6); // upper 2 bit 78 | 79 | // Basic Header ChunkID 80 | let csid = data[header_length++] & 0x3f; // lower 6 bits 81 | 82 | if(csid === 0) { // csid is 14bit 83 | csid = data[header_length++] + 64; 84 | 85 | } else if (csid === 1) { // csid is 22bit 86 | csid = data[header_length++] * 256 + data[header_length++] + 64; 87 | } 88 | 89 | Log.d(this.TAG, "chunk type: ", fmt, " StreamID: " + csid); 90 | 91 | let payload; 92 | 93 | // Message 94 | switch(fmt) { 95 | case 0: // 11 byte 96 | timestamp = (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); // 3 byte timestamp 97 | message_length = (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); // 3 byte Message length 98 | 99 | msg = new RTMPMessage(); 100 | msg.setMessageType(data[header_length++]); // 1 byte msg type 101 | msg.setMessageStreamID((data[header_length++] << 24) | (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++])); // 4 byte Message stream id 102 | msg.setMessageLength(message_length); 103 | 104 | if (timestamp === 0xFFFFFF) { // extended Timestamp 105 | timestamp = (data[header_length++] << 24) | (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); 106 | msg.setExtendedTimestamp(true); 107 | } 108 | 109 | msg.setMessageTimestamp(timestamp); 110 | 111 | Log.d(this.TAG, "message_length: " + message_length); 112 | 113 | this._chunkstreams[csid] = msg; 114 | break; 115 | 116 | case 1: // 7 byte 117 | timestamp = (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); // 3 byte timestamp 118 | message_length = (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); // 3 byte Message length 119 | 120 | msg = this._chunkstreams[csid]; 121 | msg.setMessageType(data[header_length++]); 122 | msg.setMessageLength(message_length); 123 | 124 | if (timestamp === 0xFFFFFF) { // extended Timestamp 125 | timestamp = (data[header_length++] << 24) | (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); 126 | msg.setExtendedTimestamp(true); 127 | } else { 128 | msg.setExtendedTimestamp(false); 129 | } 130 | 131 | msg.setTimestampDelta(timestamp); 132 | 133 | Log.d(this.TAG, "message_length: " + message_length); 134 | 135 | this._chunkstreams[csid] = msg; 136 | break; 137 | 138 | case 2: // 3 byte 139 | timestamp = (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); // 3 byte timestamp delta 140 | 141 | msg = this._chunkstreams[csid]; 142 | 143 | if (timestamp === 0xFFFFFF) { // extended Timestamp 144 | timestamp = (data[header_length++] << 24) | (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); 145 | msg.setExtendedTimestamp(true); 146 | 147 | } else { 148 | msg.setExtendedTimestamp(false); 149 | } 150 | 151 | msg.setTimestampDelta(timestamp); 152 | 153 | break; 154 | 155 | case 3: // 0 byte 156 | msg = this._chunkstreams[csid]; 157 | 158 | // extended timestamp is present when setted in the chunk stream 159 | if(msg.getExtendedTimestamp()) { 160 | timestamp = (data[header_length++] << 24) | (data[header_length++] << 16) | (data[header_length++] << 8) | (data[header_length++]); 161 | msg.setTimestampDelta(timestamp); 162 | } 163 | 164 | break; 165 | } 166 | 167 | if(!msg) { 168 | Log.e(this.TAG, "No suitable RTMPMessage found"); 169 | } 170 | 171 | 172 | 173 | payload_length = this._chunkstreams[csid].bytesMissing(); 174 | 175 | if(payload_length > this.CHUNK_SIZE) payload_length = this.CHUNK_SIZE; // Max. CHUNK_SIZE erwarten 176 | 177 | payload = data.slice(header_length, header_length +payload_length); 178 | 179 | // sind genug bytes für das chunk da? 180 | if(payload.length < payload_length){ 181 | Log.d(this.TAG, "packet(" + payload.length + "/" + payload_length + ") too small, wait for next"); 182 | return; 183 | } 184 | 185 | this._chunkstreams[csid].addPayload(payload); 186 | 187 | if(this._chunkstreams[csid].isComplete()) { // Message complete 188 | Log.d(this.TAG, "RTMP: ", msg.getMessageType(), RTMPMessage.MessageTypes[msg.getMessageType()], msg.getPayloadlength(), msg.getMessageStreamID()); 189 | this.conn_worker.onMessage(this._chunkstreams[csid]); 190 | this._chunkstreams[csid].clearPayload(); 191 | } 192 | 193 | let consumed = (header_length + payload_length); 194 | 195 | if(consumed > this._buffer.length) { 196 | Log.w(this.TAG, "mehr abschneiden als da"); 197 | } 198 | 199 | this._buffer = this._buffer.slice(consumed); 200 | Log.d(this.TAG, "consumed: " + consumed + " bytes, rest: " + this._buffer.length); 201 | 202 | } while(this._buffer.length > 11); // minimum size 203 | 204 | Log.d(this.TAG, "parseChunk complete"); 205 | } 206 | 207 | 208 | 209 | /** 210 | * Sets the chunk_size 211 | * @param {Number} size 212 | */ 213 | setChunkSize(size){ 214 | Log.d(this.TAG, "SetChunkSize: " + size); 215 | this.CHUNK_SIZE = size; 216 | } 217 | } 218 | 219 | export default ChunkParser; 220 | -------------------------------------------------------------------------------- /src/rtmp/NetConnection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import ProtocolControlMessage from "./ProtocolControlMessage"; 22 | import RTMPMessage from "./RTMPMessage"; 23 | import Chunk from "./Chunk"; 24 | import Log from "../utils/logger"; 25 | 26 | class NetConnection{ 27 | TAG = "NetConnection"; 28 | WindowAcknowledgementSize; 29 | MessageStreamID; 30 | CHUNK_SIZE = 128; 31 | BandWidth; 32 | socket; 33 | 34 | /** 35 | * 36 | * @param {Number} message_stream_id 37 | * @param {RTMPMessageHandler} handler 38 | */ 39 | constructor(message_stream_id, handler) { 40 | this.MessageStreamID = message_stream_id; 41 | 42 | Log.d(this.TAG, handler); 43 | 44 | this.handler = handler; 45 | this.socket = handler.socket; 46 | } 47 | 48 | /** 49 | * 50 | * @param {RTMPMessage} message 51 | */ 52 | parseMessage(message){ // RTMPMessage 53 | let data = message.getPayload(); 54 | 55 | switch(message.getMessageType()){ 56 | case 1: // PCM Set Chunk Size 57 | this.CHUNK_SIZE = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 58 | this.handler.setChunkSize(this.CHUNK_SIZE) 59 | break; 60 | 61 | case 2: // PCM Abort Message 62 | case 3: // PCM Acknowledgement 63 | case 5: // PCM Window Acknowledgement Size 64 | this.WindowAcknowledgementSize = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 65 | Log.i(this.TAG, "WindowAcknowledgementSize: " + this.WindowAcknowledgementSize); 66 | break; 67 | 68 | case 6: // PCM Set Peer Bandwidth 69 | this.BandWidth = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 70 | Log.i(this.TAG, "SetPeerBandwidth: " + this.BandWidth); 71 | 72 | // send Window Ack Size 73 | let msg = new ProtocolControlMessage(0x05, this.WindowAcknowledgementSize); 74 | 75 | let m2 = new RTMPMessage(msg.getBytes()); 76 | m2.setMessageType(0x05) // WinACKSize 77 | 78 | const chunk = new Chunk(m2); 79 | chunk.setChunkStreamID(2); // Control Channel 80 | 81 | Log.i(this.TAG, "send WindowAcksize"); 82 | this.socket.send(chunk.getBytes()); 83 | 84 | break; 85 | 86 | default: 87 | break; 88 | } 89 | } 90 | } 91 | 92 | export default NetConnection; 93 | -------------------------------------------------------------------------------- /src/rtmp/ProtocolControlMessage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "../utils/logger"; 22 | 23 | class ProtocolControlMessage{ 24 | TAG = "ProtocolControlMessage"; 25 | pcm_type; 26 | data; 27 | 28 | static pcm_types = ["dummy", "SetChunkSize", "AbortMessage", "Acknowledgement", "UserControlMessage", "WindowAcknowledgementSize", "SetPeerBandwidth"]; 29 | 30 | constructor(pcm_type, data) { 31 | switch(pcm_type){ 32 | case 1: 33 | case 2: 34 | case 3: 35 | case 5: 36 | this.pcm_type = pcm_type; 37 | this.data = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 38 | break; 39 | 40 | case 6: 41 | Log.w(this.TAG, "Protocol Control Message Type: " + pcm_type + " use SetPeerBandwidthMessage"); 42 | break; 43 | 44 | default: 45 | Log.e(this.TAG, "Protocol Control Message Type: " + pcm_type + " not supported"); 46 | break; 47 | } 48 | } 49 | 50 | setPayload(data){ 51 | this.data = data; 52 | } 53 | 54 | getEventMessage(){ 55 | let o = {}; 56 | o[ProtocolControlMessage.pcm_types[this.pcm_type]] = this.data; 57 | return o; 58 | } 59 | 60 | getBytes(){ 61 | let ret = []; 62 | 63 | ret[0] = (this.data >>> 24); 64 | ret[1] = (this.data >>> 16); 65 | ret[2] = (this.data >>> 8); 66 | ret[3] = (this.data); 67 | 68 | return new Uint8Array(ret); 69 | } 70 | } 71 | export default ProtocolControlMessage; 72 | -------------------------------------------------------------------------------- /src/rtmp/RTMPHandshake.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "../utils/logger"; 22 | 23 | /** 24 | * Class for handle the rtmp handshake 25 | */ 26 | class RTMPHandshake{ 27 | TAG = "RTMPHandshake"; 28 | state = 0; 29 | onHandshakeDone = null; 30 | c1; 31 | c2; 32 | 33 | /** 34 | * 35 | * @param {WebSocket} socket 36 | */ 37 | constructor(socket) { 38 | this.socket = socket; 39 | 40 | this.socket.onmessage = (e)=>{ 41 | Log.v(this.TAG, e.data); 42 | this.processServerInput(new Uint8Array(e.data)); 43 | } 44 | } 45 | 46 | /** 47 | * Do RTMP Handshake 48 | */ 49 | do(){ 50 | if(!this.onHandshakeDone) { 51 | Log.e(this.TAG, "onHandshakeDone not defined"); 52 | return; 53 | } 54 | 55 | Log.v(this.TAG, "send C0"); 56 | this.socket.send(new Uint8Array([0x03])); 57 | this.state = 1; 58 | 59 | Log.v(this.TAG, "send C1"); 60 | this.socket.send(this._generateC1()); 61 | this.state = 2; 62 | } 63 | 64 | _generateC1(){ 65 | const c1 = new Uint8Array(1536); 66 | 67 | for(let i = 0; i < c1.length; i++) { 68 | c1[i] = Math.floor(Math.random() * 256); 69 | } 70 | 71 | let time = Math.round(Date.now() / 1000); 72 | 73 | c1[0] = (time >>> 24); 74 | c1[1] = (time >>> 16); 75 | c1[2] = (time >>> 8); 76 | c1[3] = (time); 77 | 78 | c1[4] = 0; 79 | c1[5] = 0; 80 | c1[6] = 0; 81 | c1[7] = 0; 82 | 83 | this.c1 = c1; 84 | return c1; 85 | } 86 | 87 | _generateC2(s1){ 88 | this.c2 = s1; 89 | return this.c2; 90 | } 91 | 92 | /** 93 | * 94 | * @param {Uint8Array} data 95 | * @private 96 | */ 97 | _parseS0(data){ 98 | Log.v(this.TAG, "S0: ", data); 99 | 100 | if(data[0] !== 0x03) { 101 | Log.e(this.TAG, "S0 response not 0x03"); 102 | 103 | } else { 104 | Log.v(this.TAG, "1st Byte OK"); 105 | } 106 | 107 | this.state = 3; 108 | 109 | if(data.length > 1) { 110 | Log.v(this.TAG, "S1 included"); 111 | this._parseS1(data.slice(1)); 112 | } 113 | } 114 | 115 | /** 116 | * 117 | * @param {Uint8Array} data 118 | * @private 119 | */ 120 | _parseS1(data){ 121 | Log.v(this.TAG, "parse S1: ", data); 122 | this.state = 4; 123 | 124 | let s1 = data.slice(0, 1536); 125 | 126 | Log.v(this.TAG, "send C2"); 127 | this.socket.send(this._generateC2(s1)); 128 | 129 | this.state = 5; 130 | 131 | if(data.length > 1536) { 132 | Log.v(this.TAG, "S2 included: " + data.length); 133 | this._parseS2(data.slice(1536)); 134 | } 135 | } 136 | 137 | /** 138 | * 139 | * @param {Uint8Array} data 140 | * @private 141 | */ 142 | _parseS2(data) { 143 | Log.v(this.TAG, "parse S2: ", data); 144 | 145 | if(!this._compare(this.c1, data)) { 146 | Log.e(this.TAG, "C1 S1 not equal"); 147 | this.onHandshakeDone(false); 148 | return; 149 | } 150 | 151 | this.state = 6; 152 | 153 | Log.v(this.TAG, "RTMP Connection established"); 154 | 155 | this.onHandshakeDone(true); 156 | } 157 | 158 | /** 159 | * compare to arrays 160 | * @param ar1 161 | * @param ar2 162 | * @returns {boolean} 163 | * @private 164 | */ 165 | _compare(ar1, ar2){ 166 | for(let i = 0; i < ar1.length; i++){ 167 | if(ar1[i] !== ar2[i]) return false; 168 | } 169 | 170 | return true; 171 | } 172 | 173 | 174 | /** 175 | * 176 | * @param {Uint8Array} data 177 | */ 178 | processServerInput(data){ 179 | switch(this.state){ 180 | case 2: // 181 | this._parseS0(data); 182 | break; 183 | 184 | case 3: 185 | this._parseS1(data); 186 | break; 187 | 188 | case 5: 189 | this._parseS2(data); 190 | break; 191 | } 192 | } 193 | } 194 | 195 | export default RTMPHandshake; 196 | -------------------------------------------------------------------------------- /src/rtmp/RTMPMessage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import {_concatArrayBuffers} from "../utils/utils"; 22 | import Log from "../utils/logger"; 23 | 24 | /** 25 | * Class which represent a RTMP Message 26 | */ 27 | class RTMPMessage{ 28 | TAG = "RTMPMessage"; 29 | 30 | static MessageTypes = ["dummy", "PCMSetChunkSize", "PCMAbortMessage", "PCMAcknolegement", "UserControlMessage", "WindowAcknowledgementSize", "PCMSetPeerBandwidth", 31 | "dummy", "AudioMessage", "VideoMessage", "dummy", "dummy", "dummy", "dummy", "dummy", "DataMessageAMF3", "Shared Object Message AMF3", "CommandMessageAMF3", 32 | "DataMessageAMF0", "SharedObjectMessageAMF0", "CommandMessageAMF0", "dummy", "Aggregate Message"]; 33 | 34 | messageType; 35 | messageLength = 0; 36 | length = 0; 37 | timestamp = 0; 38 | extendedTimestamp = false; 39 | message_stream_id = 0; 40 | payload = new Uint8Array(0); 41 | 42 | /** 43 | * 44 | * @param {Uint8Array} payload 45 | */ 46 | constructor(payload) { 47 | if(payload) { 48 | this.setMessageLength(payload.length); 49 | this.addPayload(payload); 50 | } 51 | } 52 | 53 | clearPayload(){ 54 | this.payload = new Uint8Array(0); 55 | } 56 | 57 | /** 58 | * 59 | * @returns {Uint8Array} 60 | */ 61 | getBytes(){ 62 | this.header = new Uint8Array(11); 63 | this.header[0] = this.messageType; 64 | 65 | this.header[1] = (this.length >>> 16); 66 | this.header[2] = (this.length >>> 8); 67 | this.header[3] = (this.length); 68 | 69 | this.header[4] = (this.timestamp >>> 24); 70 | this.header[5] = (this.timestamp >>> 16); 71 | this.header[6] = (this.timestamp >>> 8); 72 | this.header[7] = (this.timestamp); 73 | 74 | this.header[8] = (this.message_stream_id >>> 16); 75 | this.header[9] = (this.message_stream_id >>> 8); 76 | this.header[10] = (this.message_stream_id); 77 | 78 | return _concatArrayBuffers(this.header, this.payload); 79 | } 80 | 81 | /** 82 | * 83 | * @param {Number} message_type 84 | */ 85 | setMessageType(message_type){ 86 | this.messageType = message_type; 87 | switch(message_type){ 88 | case 1: // setBandwidth 89 | case 2: 90 | case 3: 91 | case 4: // UserControlMSG 92 | case 5: 93 | case 6: 94 | this.message_stream_id = 0; 95 | break; 96 | } 97 | } 98 | 99 | getMessageType(){ 100 | return this.messageType; 101 | } 102 | 103 | getMessageStreamID(){ 104 | return this.message_stream_id; 105 | } 106 | 107 | setMessageStreamID(messageStreamID) { 108 | this.message_stream_id = messageStreamID; 109 | } 110 | 111 | getPayloadlength(){ 112 | return this.payload.length; 113 | } 114 | 115 | getTimestamp(){ 116 | return this.timestamp; 117 | } 118 | 119 | setMessageTimestamp(timestamp) { 120 | Log.v(this.TAG, "TS: " + timestamp); 121 | this.timestamp = timestamp; 122 | } 123 | 124 | /** 125 | * 126 | * @param {boolean} yes 127 | */ 128 | setExtendedTimestamp(yes){ 129 | Log.w(this.TAG, "setExtendedTimestamp"); 130 | this.extendedTimestamp = yes; 131 | } 132 | 133 | getExtendedTimestamp(){ 134 | return this.extendedTimestamp; 135 | } 136 | 137 | setTimestampDelta(timestamp_delta){ 138 | Log.v(this.TAG, "TS: " + this.timestamp + " Delta: " + timestamp_delta); 139 | this.timestamp += timestamp_delta; 140 | } 141 | 142 | /** 143 | * 144 | * @param {Uint8Array} data 145 | */ 146 | addPayload(data){ 147 | if(data.length > this.bytesMissing()) { 148 | Log.e(this.TAG, "try to add too much data"); 149 | return; 150 | } 151 | 152 | this.payload = _concatArrayBuffers(this.payload, data); 153 | this.length = this.payload.length; 154 | Log.d(this.TAG, "[ RTMPMessage ] payload size is now: " + this.length); 155 | } 156 | 157 | getPayload(){ 158 | return this.payload; 159 | } 160 | 161 | setMessageLength(message_length) { 162 | this.messageLength = message_length; 163 | } 164 | 165 | getMessageLength(){ 166 | return this.messageLength; 167 | } 168 | 169 | isComplete(){ 170 | if(this.payload.length === this.messageLength) return true; 171 | return false; 172 | } 173 | 174 | bytesMissing(){ 175 | return this.messageLength - this.payload.length; 176 | } 177 | } 178 | 179 | export default RTMPMessage; 180 | -------------------------------------------------------------------------------- /src/rtmp/RTMPMessageHandler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import RTMPMessage from "./RTMPMessage"; 22 | import Chunk from "./Chunk"; 23 | import UserControlMessage from "./UserControlMessage"; 24 | import NetConnection from "./NetConnection"; 25 | import ChunkParser from "./ChunkParser"; 26 | import RTMPMediaMessageHandler from "./RTMPMediaMessageHandler"; 27 | import AMF0Object from "./AMF0Object"; 28 | import Log from "../utils/logger"; 29 | 30 | /** 31 | * Class for handling rtmp messages 32 | */ 33 | class RTMPMessageHandler { 34 | TAG = "RTMPMessageHandler"; 35 | 36 | paused = false; 37 | netconnections = {}; 38 | chunk_stream_id = 2; 39 | trackedCommand = ""; 40 | socket; 41 | current_stream_id; 42 | 43 | /** 44 | * 45 | * @param {WebSocket} socket 46 | */ 47 | constructor(socket) { 48 | this.socket = socket; 49 | this.chunk_parser = new ChunkParser(this); 50 | this.media_handler = new RTMPMediaMessageHandler(); 51 | 52 | this.media_handler.onError = (type, info)=>{ 53 | Log.d(this.TAG, type, info); 54 | postMessage(["onError", type, info]); 55 | } 56 | 57 | this.media_handler.onMediaInfo = (mediainfo)=>{ 58 | Log.d(this.TAG, mediainfo); 59 | postMessage(["onMediaInfo", mediainfo]); 60 | } 61 | 62 | this.media_handler.onMetaDataArrived = (metadata)=>{ 63 | postMessage(["onMetaDataArrived", metadata]); 64 | } 65 | 66 | this.media_handler.onScriptDataArrived= (data)=>{ 67 | postMessage(["onScriptDataArrived", data]); 68 | } 69 | 70 | this.media_handler.onScriptDataArrived= (data)=>{ 71 | postMessage(["onMetaDataArrived", data]); 72 | } 73 | 74 | this.media_handler.onScriptDataArrived= (data)=>{ 75 | postMessage(["onMetaDataArrived", data]); 76 | } 77 | } 78 | 79 | destroy(){ 80 | this.media_handler.destroy(); 81 | this.media_handler = null; 82 | this.chunk_parser = null 83 | } 84 | 85 | /** 86 | * 87 | * @param {Uint8Array} data 88 | */ 89 | parseChunk(data){ 90 | Log.d(this.TAG, "parseChunk: " + data.length); 91 | this.chunk_parser.parseChunk(data); 92 | } 93 | 94 | /** 95 | * 96 | * @param {RTMPMessage} msg 97 | */ 98 | onMessage(msg){ 99 | Log.d(this.TAG, " onMessage: " + msg.getMessageType() + " StreamID:" + msg.getMessageStreamID()); 100 | 101 | switch(msg.getMessageType()){ 102 | case 1: // PCM Set Chunk Size 103 | case 2: // PCM Abort Message 104 | case 3: // PCM Acknowledgement 105 | case 5: // PCM Window Acknowledgement Size 106 | case 6: // PCM Set Peer Bandwidth 107 | this.netconnections[msg.getMessageStreamID()].parseMessage(msg); 108 | break; 109 | 110 | case 4: // User Control Messages 111 | this._handleUserControlMessage(msg); 112 | break; 113 | 114 | case 8: // Audio Message 115 | Log.d(this.TAG, "AUDIOFRAME: ", msg); 116 | this.media_handler.handleMediaMessage(msg); 117 | break; 118 | 119 | case 9: // Video Message 120 | Log.d(this.TAG, "VIDEOFRAME: ", msg); 121 | this.media_handler.handleMediaMessage(msg); 122 | break; 123 | 124 | case 18: // Data Message AMF0 125 | Log.d(this.TAG, "DATAFRAME: ", msg); 126 | this.media_handler.handleMediaMessage(msg); 127 | break; 128 | 129 | case 19: // Shared Object Message AMF0 130 | Log.d(this.TAG, "SharedObjectMessage", msg); 131 | break; 132 | 133 | case 20: // Command Message AMF0 134 | const command = new AMF0Object(); 135 | let cmd = command.parseAMF0(msg.getPayload()); 136 | 137 | Log.d(this.TAG, "AMF0", cmd); 138 | 139 | switch(cmd[0]) { 140 | case "_error": 141 | Log.e(this.TAG, cmd); 142 | break; 143 | 144 | case "_result": 145 | switch(this.trackedCommand){ 146 | case "connect": 147 | Log.d(this.TAG,"got _result: " + cmd[3].code); 148 | if(cmd[3].code === "NetConnection.Connect.Success") { 149 | postMessage([cmd[3].code]); 150 | this.createStream(null); 151 | } 152 | break; 153 | 154 | case "createStream": 155 | Log.d(this.TAG,"got _result: " + cmd[3]); 156 | if(cmd[3]) { 157 | this.current_stream_id = cmd[4]; 158 | postMessage(["RTMPStreamCreated", cmd[3], cmd[4]]); 159 | } 160 | break; 161 | 162 | case "play": 163 | break; 164 | 165 | case "pause": 166 | break; 167 | 168 | default: 169 | Log.w("tracked command:" + this.trackedCommand); 170 | break; 171 | } 172 | 173 | break; 174 | 175 | case "onStatus": 176 | Log.d(this.TAG,"onStatus: " + cmd[3].code); 177 | postMessage([cmd[3].code]); 178 | break; 179 | 180 | default: 181 | Log.w(this.TAG,"CommandMessage " + cmd[0] + " not yet implemented"); 182 | break; 183 | } 184 | 185 | break; 186 | 187 | case 22: // Aggregate Message 188 | break; 189 | 190 | case 15: // Data Message AMF3 191 | case 16: // Shared Object Message AMF3 192 | case 17: // Command Message AMF3 193 | Log.e(this.TAG,"AMF3 is not yet implemented"); 194 | break; 195 | 196 | default: 197 | Log.d(this.TAG,"[MessageType: " + RTMPMessage.MessageTypes[msg.getMessageType()] + "(" + msg.getMessageType() + ")"); 198 | break; 199 | 200 | } 201 | } 202 | 203 | /** 204 | * 205 | * @param {Object} connectionParams 206 | */ 207 | connect(connectionParams){ 208 | const command = new AMF0Object([ 209 | "connect", 1, connectionParams 210 | ]); 211 | 212 | this._sendCommand(3, command); 213 | } 214 | 215 | /** 216 | * 217 | * @param {Object} options 218 | */ 219 | createStream(options){ 220 | const command = new AMF0Object([ 221 | "createStream", 1, options 222 | ]); 223 | 224 | this._sendCommand(3, command); 225 | } 226 | 227 | deleteStream(stream_id){ 228 | const command = new AMF0Object([ 229 | "deleteStream", 1, null, stream_id 230 | ]); 231 | 232 | this._sendCommand(3, command); 233 | } 234 | 235 | /** 236 | * 237 | * @param {String} streamName 238 | */ 239 | play(streamName){ 240 | const command = new AMF0Object([ 241 | "play", 1, null, streamName 242 | ]); 243 | 244 | this._sendCommand(3, command); 245 | } 246 | 247 | stop(){ 248 | this.deleteStream(this.current_stream_id); 249 | } 250 | 251 | /** 252 | * 253 | * @param {boolean} enable 254 | */ 255 | pause(enable){ 256 | if(this.paused !== enable) { 257 | this.paused = enable; 258 | 259 | const command = new AMF0Object([ 260 | "pause", 0, null, enable,0 261 | ]); 262 | 263 | this._sendCommand(3, command); 264 | } 265 | } 266 | 267 | receiveVideo(enable){ 268 | const command = new AMF0Object([ 269 | "receiveVideo", 0, null, enable 270 | ]); 271 | 272 | this._sendCommand(3, command); 273 | } 274 | 275 | receiveAudio(enable){ 276 | const command = new AMF0Object([ 277 | "receiveAudio", 0, null, enable 278 | ]); 279 | 280 | this._sendCommand(3, command); 281 | } 282 | 283 | /** 284 | * 285 | * @param {Number} csid 286 | * @param {AMF0Object} command 287 | * @private 288 | */ 289 | _sendCommand(csid, command){ 290 | Log.d(this.TAG, "sendCommand:", command); 291 | 292 | this.trackedCommand = command.getCommand(); 293 | 294 | let msg = new RTMPMessage(command.getBytes()); 295 | msg.setMessageType(0x14); // AMF0 Command 296 | msg.setMessageStreamID(0); 297 | 298 | const chunk = new Chunk(msg); 299 | chunk.setChunkStreamID(csid); 300 | 301 | let buf = chunk.getBytes(); 302 | 303 | this.netconnections[0] = new NetConnection(0, this); 304 | 305 | this.socket.send(buf); 306 | } 307 | 308 | /** 309 | * 310 | * @param {Number} size 311 | */ 312 | setChunkSize(size){ 313 | this.chunk_parser.setChunkSize(size); 314 | } 315 | 316 | _getNextMessageStreamID(){ 317 | return this.netconnections.length; 318 | } 319 | 320 | _getNextChunkStreamID(){ 321 | return ++this.chunk_stream_id; // increase chunk stream id 322 | } 323 | 324 | /** 325 | * 326 | * @param {RTMPMessage} msg 327 | * @private 328 | */ 329 | _handleUserControlMessage(msg) { 330 | let data = msg.getPayload() 331 | 332 | this.event_type = (data[0] <<8) | data[1]; 333 | data = data.slice(2); 334 | 335 | switch (this.event_type){ 336 | case 0x00: // StreamBegin 337 | case 0x01: // Stream EOF 338 | case 0x02: // StreamDry 339 | case 0x04: // StreamIsRecorded 340 | this.event_data1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 341 | break; 342 | 343 | 344 | case 0x03: // SetBuffer 345 | this.event_data1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 346 | this.event_data2 = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | (data[7]); 347 | break; 348 | 349 | case 0x06: // PingRequest 350 | case 0x07: // PingResponse 351 | this.event_data1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); 352 | break; 353 | } 354 | 355 | // Handle Ping internal 356 | if(this.event_type === 0x06) { // Ping Request 357 | postMessage(["UserControlMessage", ["ping", this.event_data1]]); 358 | 359 | const msg = new UserControlMessage(); 360 | msg.setType(0x07); // Ping Response 361 | msg.setEventData(this.event_data1); 362 | 363 | 364 | let m2 = new RTMPMessage(msg.getBytes()); 365 | m2.setMessageType(0x04) // UserControlMessage 366 | 367 | const chunk = new Chunk(m2); 368 | chunk.setChunkStreamID(2); // Control Channel 369 | 370 | Log.i(this.TAG,"send Pong"); 371 | this.socket.send(chunk.getBytes()); 372 | } 373 | } 374 | } 375 | 376 | export default RTMPMessageHandler; 377 | 378 | -------------------------------------------------------------------------------- /src/rtmp/UserControlMessage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | class UserControlMessage{ 22 | event_type; 23 | event_data1; 24 | event_data2; 25 | 26 | static events = ["StreamBegin", "StreamEOF", "StreamDry", "SetBuffer", "StreamIsRecorded", "dummy", "PingRequest", "PingResponse"]; 27 | 28 | /** 29 | * 30 | * @returns {Uint8Array} 31 | */ 32 | getBytes(){ 33 | let ret; 34 | 35 | if(this.event_data2) { 36 | ret = new Uint8Array(10); 37 | ret[0] = (this.event_type >>> 8); 38 | ret[1] = (this.event_type); 39 | 40 | ret[2] = (this.event_data1 >>> 24); 41 | ret[3] = (this.event_data1 >>> 16); 42 | ret[4] = (this.event_data1 >>> 8); 43 | ret[5] = (this.event_data1); 44 | 45 | ret[6] = (this.event_data2 >>> 24); 46 | ret[7] = (this.event_data2 >>> 16); 47 | ret[8] = (this.event_data2 >>> 8); 48 | ret[9] = (this.event_data2); 49 | 50 | } else { 51 | ret = new Uint8Array(6); 52 | ret[0] = (this.event_type >>> 8); 53 | ret[1] = (this.event_type); 54 | 55 | ret[2] = (this.event_data1 >>> 24); 56 | ret[3] = (this.event_data1 >>> 16); 57 | ret[4] = (this.event_data1 >>> 8); 58 | ret[5] = (this.event_data1); 59 | } 60 | 61 | return ret; 62 | } 63 | 64 | getEventMessage(){ 65 | let o = {}; 66 | 67 | if(this.event_type === 3) { 68 | o[UserControlMessage.events[this.event_type]] = [this.event_data1, this.event_data2]; 69 | } else { 70 | o[UserControlMessage.events[this.event_type]] = this.event_data1; 71 | } 72 | 73 | return o; 74 | } 75 | 76 | setType(event_type){ 77 | this.event_type = event_type; 78 | } 79 | 80 | setEventData(event_data){ 81 | this.event_data1 = event_data; 82 | } 83 | } 84 | 85 | export default UserControlMessage; 86 | -------------------------------------------------------------------------------- /src/utils/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | let Browser = {}; 20 | 21 | function detect() { 22 | // modified from jquery-browser-plugin 23 | 24 | let ua = self.navigator.userAgent.toLowerCase(); 25 | 26 | let match = /(edge)\/([\w.]+)/.exec(ua) || 27 | /(opr)[\/]([\w.]+)/.exec(ua) || 28 | /(chrome)[ \/]([\w.]+)/.exec(ua) || 29 | /(iemobile)[\/]([\w.]+)/.exec(ua) || 30 | /(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || 31 | /(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || 32 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 33 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 34 | /(msie) ([\w.]+)/.exec(ua) || 35 | ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) || 36 | ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) || 37 | []; 38 | 39 | let platform_match = /(ipad)/.exec(ua) || 40 | /(ipod)/.exec(ua) || 41 | /(windows phone)/.exec(ua) || 42 | /(iphone)/.exec(ua) || 43 | /(kindle)/.exec(ua) || 44 | /(android)/.exec(ua) || 45 | /(windows)/.exec(ua) || 46 | /(mac)/.exec(ua) || 47 | /(linux)/.exec(ua) || 48 | /(cros)/.exec(ua) || 49 | []; 50 | 51 | let matched = { 52 | browser: match[5] || match[3] || match[1] || '', 53 | version: match[2] || match[4] || '0', 54 | majorVersion: match[4] || match[2] || '0', 55 | platform: platform_match[0] || '' 56 | }; 57 | 58 | let browser = {}; 59 | if (matched.browser) { 60 | browser[matched.browser] = true; 61 | 62 | let versionArray = matched.majorVersion.split('.'); 63 | browser.version = { 64 | major: parseInt(matched.majorVersion, 10), 65 | string: matched.version 66 | }; 67 | if (versionArray.length > 1) { 68 | browser.version.minor = parseInt(versionArray[1], 10); 69 | } 70 | if (versionArray.length > 2) { 71 | browser.version.build = parseInt(versionArray[2], 10); 72 | } 73 | } 74 | 75 | if (matched.platform) { 76 | browser[matched.platform] = true; 77 | } 78 | 79 | if (browser.chrome || browser.opr || browser.safari) { 80 | browser.webkit = true; 81 | } 82 | 83 | // MSIE. IE11 has 'rv' identifer 84 | if (browser.rv || browser.iemobile) { 85 | if (browser.rv) { 86 | delete browser.rv; 87 | } 88 | let msie = 'msie'; 89 | matched.browser = msie; 90 | browser[msie] = true; 91 | } 92 | 93 | // Microsoft Edge 94 | if (browser.edge) { 95 | delete browser.edge; 96 | let msedge = 'msedge'; 97 | matched.browser = msedge; 98 | browser[msedge] = true; 99 | } 100 | 101 | // Opera 15+ 102 | if (browser.opr) { 103 | let opera = 'opera'; 104 | matched.browser = opera; 105 | browser[opera] = true; 106 | } 107 | 108 | // Stock android browsers are marked as Safari 109 | if (browser.safari && browser.android) { 110 | let android = 'android'; 111 | matched.browser = android; 112 | browser[android] = true; 113 | } 114 | 115 | browser.name = matched.browser; 116 | browser.platform = matched.platform; 117 | 118 | for (let key in Browser) { 119 | if (Browser.hasOwnProperty(key)) { 120 | delete Browser[key]; 121 | } 122 | } 123 | Object.assign(Browser, browser); 124 | } 125 | 126 | detect(); 127 | 128 | export default Browser; 129 | -------------------------------------------------------------------------------- /src/utils/event_emitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "../utils/logger"; 22 | 23 | /** 24 | * A small class for Handling events 25 | */ 26 | class EventEmitter{ 27 | ListenerList = []; 28 | TAG = "EventEmitter"; 29 | waiters = []; 30 | 31 | constructor() { 32 | } 33 | 34 | /** 35 | * Add an event listener 36 | * @param {String} event - The Name of the event 37 | * @param {Function} listener - the callback when occurs 38 | * @param {boolean} modal - Overwrite existing listener for this event 39 | */ 40 | addEventListener(event, listener, modal = false){ 41 | Log.d(this.TAG, "addEventListener: " + event); 42 | 43 | for(let i = 0; i < this.ListenerList.length;i++){ 44 | let entry = this.ListenerList[i]; 45 | if(entry[0] === event) { 46 | if (modal || entry[1] === listener) { 47 | Log.w(this.TAG, "Listener already registered, overriding"); 48 | return; 49 | } 50 | } 51 | } 52 | this.ListenerList.push([event, listener]); 53 | } 54 | 55 | waitForEvent(event, callback){ 56 | this.waiters.push([event, callback]); 57 | } 58 | 59 | /** 60 | * A synonym for addEventListener 61 | * @param {String} event 62 | * @param {Function} listener 63 | * @param {boolean} modal 64 | */ 65 | addListener(event, listener, modal){ 66 | this.addEventListener(event, listener, modal); 67 | } 68 | 69 | 70 | /** 71 | * Remove an event listener 72 | * @param {String} event 73 | * @param {Function} listener 74 | */ 75 | removeEventListener(event, listener){ 76 | Log.d(this.TAG, "removeEventListener: " + event); 77 | 78 | for(let i = 0; i < this.ListenerList.length;i++){ 79 | let entry = this.ListenerList[i]; 80 | if(entry[0] === event && entry[1] === listener){ 81 | this.ListenerList.splice(i,1); 82 | return; 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * A synonym for removeEventListener 89 | * @param {String} event 90 | * @param {Function} listener 91 | */ 92 | removeListener(event, listener){ 93 | this.removeEventListener(event, listener); 94 | } 95 | 96 | /** 97 | * Remove all listener 98 | * @param {String|undefined} event - If provided, remove all listener for this event 99 | */ 100 | removeAllEventListener(event){ 101 | Log.d(this.TAG, "removeAllEventListener: ", event); 102 | if(event) { 103 | for(let i = 0; i < this.ListenerList.length;i++) { 104 | let entry = this.ListenerList[i]; 105 | if(entry[0] === event){ 106 | this.ListenerList.splice(i,1); 107 | i--; 108 | } 109 | } 110 | } else 111 | this.ListenerList = []; 112 | } 113 | 114 | /** 115 | * A synonym for removeAllEventListener 116 | * @param event 117 | */ 118 | removeAllListener(event){ 119 | this.removeAllEventListener(event); 120 | } 121 | 122 | /** 123 | * 124 | * @param {String} event 125 | * @param data 126 | */ 127 | emit(event, ...data){ 128 | Log.t(this.TAG, "emit EVENT: " + event, ...data); 129 | 130 | for(let i = 0; i < this.waiters.length;i++){ 131 | let entry = this.waiters[i]; 132 | 133 | if(entry[0] === event){ 134 | Log.d(this.TAG, "hit waiting event: " + event); 135 | entry[1].call(this, ...data); 136 | this.waiters.splice(i,1); 137 | i--; 138 | } 139 | } 140 | 141 | for(let i = 0; i < this.ListenerList.length;i++){ 142 | let entry = this.ListenerList[i]; 143 | if(entry[0] === event){ 144 | entry[1].call(this, ...data); 145 | } 146 | } 147 | } 148 | } 149 | 150 | export default EventEmitter; 151 | 152 | -------------------------------------------------------------------------------- /src/utils/exception.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | export class RuntimeException { 20 | constructor(message) { 21 | this._message = message; 22 | } 23 | 24 | get name() { 25 | return 'RuntimeException'; 26 | } 27 | 28 | get message() { 29 | return this._message; 30 | } 31 | 32 | toString() { 33 | return this.name + ': ' + this.message; 34 | } 35 | } 36 | 37 | export class IllegalStateException extends RuntimeException { 38 | constructor(message) { 39 | super(message); 40 | } 41 | 42 | get name() { 43 | return 'IllegalStateException'; 44 | } 45 | } 46 | 47 | export class InvalidArgumentException extends RuntimeException { 48 | constructor(message) { 49 | super(message); 50 | } 51 | 52 | get name() { 53 | return 'InvalidArgumentException'; 54 | } 55 | } 56 | 57 | export class NotImplementedException extends RuntimeException { 58 | constructor(message) { 59 | super(message); 60 | } 61 | 62 | get name() { 63 | return 'NotImplementedException'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 itNOX. All Rights Reserved. 3 | * 4 | * @author Michael Balen 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | class Log { 21 | static OFF = -1; 22 | static TRACE = 0; 23 | static DEBUG = 1; 24 | static INFO = 2; 25 | static WARN = 3; 26 | static ERROR = 4; 27 | static CRITICAL = 5; 28 | static WITH_STACKTRACE = true; 29 | 30 | static LEVEL = Log.INFO; 31 | 32 | /** 33 | * Object with [ClassName, Loglevel] 34 | * @type {Object} 35 | */ 36 | static loglevels = {}; 37 | 38 | /** 39 | * 40 | * @param {Number} level 41 | * @param {String} tag 42 | * @param txt 43 | * @private 44 | */ 45 | static _output = function output(level, tag, ...txt){ 46 | let tmpLevel = Log.LEVEL; 47 | 48 | // Dirty fix because inline worker cant access static properties 49 | try{ 50 | if(Log.loglevels[tag]) tmpLevel = Log.loglevels[tag]; 51 | }catch (e) { 52 | return; 53 | } 54 | 55 | 56 | if(tmpLevel === Log.OFF) return; 57 | if(tmpLevel > level) return; 58 | 59 | const callstack = Log._getStackTrace(); 60 | 61 | // debug aufruf entfernen 62 | callstack.shift(); 63 | callstack.shift(); 64 | 65 | let color = "color: silver"; 66 | 67 | switch(level) { 68 | case Log.TRACE: // TRACE 69 | color = "background-color: gray"; 70 | break; 71 | 72 | case Log.DEBUG: // DEBUG 73 | break; 74 | 75 | case Log.INFO: // INFO 76 | color = "color: green"; 77 | break; 78 | 79 | case Log.WARN: // WARN 80 | color = "color: orange; background-color: #EAA80035"; 81 | break; 82 | 83 | case Log.ERROR: // ERROR 84 | color = "color: red; background-color: #FF000020"; 85 | break; 86 | 87 | case Log.CRITICAL: // CRITICAL 88 | color = "color: red"; 89 | break; 90 | } 91 | 92 | Log._print(callstack, color, tag, ...txt); 93 | }; 94 | 95 | /** 96 | * Internal for console dump 97 | * @param {String[]} callstack 98 | * @param {String} color 99 | * @param {String} tag 100 | * @param txt 101 | * @private 102 | */ 103 | static _print(callstack, color, tag, ...txt){ 104 | if(Log.WITH_STACKTRACE){ 105 | if(Log.LEVEL === Log.ERROR){ 106 | console.group("%c[" + tag + "]", color, ...txt); 107 | } else { 108 | console.groupCollapsed("%c[" + tag + "]", color, ...txt); 109 | } 110 | 111 | for(let i = 0; i < callstack.length; i++) { 112 | console.log("%c" + callstack[i], color); 113 | } 114 | 115 | console.groupEnd(); 116 | 117 | } else { 118 | console.log("%c[" + tag + "]", color, ...txt) 119 | } 120 | } 121 | 122 | /** 123 | * Get Callstack 124 | * @returns {String[]} 125 | * @private 126 | */ 127 | static _getStackTrace = function() { 128 | let callstack = []; 129 | 130 | try { 131 | i.dont.exist+=0; //doesn't exist- that's the point 132 | 133 | } catch(e) { 134 | if (e.stack) { //Firefox 135 | let lines = e.stack.split('\n'); 136 | 137 | for (let i=0; i < lines.length; i++) { 138 | callstack.push(lines[i]); 139 | } 140 | 141 | //Ersten Eintrag entfernen 142 | callstack.shift(); 143 | callstack.shift(); 144 | } 145 | } 146 | 147 | return(callstack); 148 | }; 149 | 150 | /** 151 | * Log Critical 152 | * @param {String} tag 153 | * @param msg 154 | */ 155 | static c(tag, ...msg) { 156 | Log._output(Log.CRITICAL, tag, ...msg); 157 | } 158 | 159 | /** 160 | * Log Error 161 | * @param {String} tag 162 | * @param msg 163 | */ 164 | static e(tag, ...msg) { 165 | Log._output(Log.ERROR, tag, ...msg); 166 | } 167 | 168 | /** 169 | * Log Info 170 | * @param {String} tag 171 | * @param msg 172 | */ 173 | static i(tag, ...msg) { 174 | Log._output(Log.INFO, tag, ...msg); 175 | } 176 | 177 | /** 178 | * Log Warning 179 | * @param {String} tag 180 | * @param msg 181 | */ 182 | static w(tag, ...msg) { 183 | Log._output(Log.WARN, tag, ...msg); 184 | } 185 | 186 | /** 187 | * Log Debug 188 | * @param {String} tag 189 | * @param msg 190 | */ 191 | static d(tag, ...msg) { 192 | Log._output(Log.DEBUG, tag, ...msg); 193 | } 194 | 195 | /** 196 | * Log Debug 197 | * @param {String} tag 198 | * @param msg 199 | */ 200 | static v(tag, ...msg) { 201 | Log._output(Log.DEBUG, tag, ...msg); 202 | } 203 | 204 | /** 205 | * Log Trace 206 | * @param {String} tag 207 | * @param msg 208 | */ 209 | static t(tag, ...msg) { 210 | Log._output(Log.TRACE, tag, ...msg); 211 | } 212 | } 213 | 214 | export default Log; 215 | -------------------------------------------------------------------------------- /src/utils/mse-controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import Log from "./logger"; 20 | import EventEmitter from "./event_emitter"; 21 | import {IDRSampleList} from "../formats/media-segment-info"; 22 | import {IllegalStateException} from "./exception"; 23 | import {MSEEvents} from "./utils"; 24 | import Browser from "./browser"; 25 | 26 | class MSEController { 27 | TAG = 'MSEController'; 28 | 29 | constructor(config) { 30 | this._config = config; 31 | this._emitter = new EventEmitter(); 32 | 33 | if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) { 34 | // For live stream, do auto cleanup by default 35 | this._config.autoCleanupSourceBuffer = true; 36 | } 37 | 38 | this.e = { 39 | onSourceOpen: this._onSourceOpen.bind(this), 40 | onSourceEnded: this._onSourceEnded.bind(this), 41 | onSourceClose: this._onSourceClose.bind(this), 42 | onSourceBufferError: this._onSourceBufferError.bind(this), 43 | onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this) 44 | }; 45 | 46 | this._mediaSource = null; 47 | this._mediaSourceObjectURL = null; 48 | this._mediaElement = null; 49 | 50 | this._isBufferFull = false; 51 | this._hasPendingEos = false; 52 | 53 | this._requireSetMediaDuration = false; 54 | this._pendingMediaDuration = 0; 55 | 56 | this._pendingSourceBufferInit = []; 57 | this._mimeTypes = { 58 | video: null, 59 | audio: null 60 | }; 61 | this._sourceBuffers = { 62 | video: null, 63 | audio: null 64 | }; 65 | this._lastInitSegments = { 66 | video: null, 67 | audio: null 68 | }; 69 | this._pendingSegments = { 70 | video: [], 71 | audio: [] 72 | }; 73 | this._pendingRemoveRanges = { 74 | video: [], 75 | audio: [] 76 | }; 77 | this._idrList = new IDRSampleList(); 78 | } 79 | 80 | destroy() { 81 | if (this._mediaElement || this._mediaSource) { 82 | this.detachMediaElement(); 83 | } 84 | this.e = null; 85 | this._emitter.removeAllListener(); 86 | this._emitter = null; 87 | } 88 | 89 | on(event, listener) { 90 | this._emitter.addListener(event, listener); 91 | } 92 | 93 | off(event, listener) { 94 | this._emitter.removeListener(event, listener); 95 | } 96 | 97 | attachMediaElement(mediaElement) { 98 | Log.i(this.TAG, "attach"); 99 | if (this._mediaSource) { 100 | throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!'); 101 | } 102 | let ms = this._mediaSource = new window.MediaSource(); 103 | ms.addEventListener('sourceopen', this.e.onSourceOpen); 104 | ms.addEventListener('sourceended', this.e.onSourceEnded); 105 | ms.addEventListener('sourceclose', this.e.onSourceClose); 106 | 107 | this._mediaElement = mediaElement; 108 | this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource); 109 | mediaElement.src = this._mediaSourceObjectURL; 110 | } 111 | 112 | detachMediaElement() { 113 | Log.i(this.TAG, "detach"); 114 | 115 | if (this._mediaSource) { 116 | let ms = this._mediaSource; 117 | 118 | if (ms.readyState === 'open') { 119 | try { 120 | ms.endOfStream(); 121 | } catch (error) { 122 | Log.e(this.TAG, error.message); 123 | } 124 | } 125 | 126 | 127 | for (let type in this._sourceBuffers) { 128 | // pending segments should be discard 129 | let ps = this._pendingSegments[type]; 130 | ps.splice(0, ps.length); 131 | this._pendingSegments[type] = null; 132 | this._pendingRemoveRanges[type] = null; 133 | this._lastInitSegments[type] = null; 134 | 135 | // remove all sourcebuffers 136 | let sb = this._sourceBuffers[type]; 137 | if (sb) { 138 | Log.i(this.TAG, "try to remove sourcebuffer: " + type); 139 | if (ms.readyState !== 'closed') { 140 | // ms edge can throw an error: Unexpected call to method or property access 141 | try { 142 | Log.i(this.TAG, "removing sourcebuffer: " + type); 143 | ms.removeSourceBuffer(sb); 144 | } catch (error) { 145 | Log.e(this.TAG, error.message); 146 | } 147 | sb.removeEventListener('error', this.e.onSourceBufferError); 148 | sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd); 149 | } 150 | this._mimeTypes[type] = null; 151 | this._sourceBuffers[type] = null; 152 | } 153 | } 154 | 155 | 156 | 157 | // proprerly remove sourcebuffers 158 | /* 159 | for(let mimeType in this._sourceBuffers) { 160 | this._mediaSource.removeSourceBuffer(this._sourceBuffers[mimeType]); 161 | }*/ 162 | 163 | ms.removeEventListener('sourceopen', this.e.onSourceOpen); 164 | ms.removeEventListener('sourceended', this.e.onSourceEnded); 165 | ms.removeEventListener('sourceclose', this.e.onSourceClose); 166 | this._pendingSourceBufferInit = []; 167 | this._isBufferFull = false; 168 | this._idrList.clear(); 169 | this._mediaSource = null; 170 | 171 | } else { 172 | Log.w(this.TAG, "no mediasource attached"); 173 | } 174 | 175 | if (this._mediaElement) { 176 | this._mediaElement.src = ''; 177 | this._mediaElement.removeAttribute('src'); 178 | this._mediaElement = null; 179 | } 180 | 181 | if (this._mediaSourceObjectURL) { 182 | window.URL.revokeObjectURL(this._mediaSourceObjectURL); 183 | this._mediaSourceObjectURL = null; 184 | } 185 | } 186 | 187 | appendInitSegment(initSegment, deferred) { 188 | Log.i(this.TAG, "appendInitSegment", initSegment); 189 | if (!this._mediaSource || this._mediaSource.readyState !== 'open') { 190 | // sourcebuffer creation requires mediaSource.readyState === 'open' 191 | // so we defer the sourcebuffer creation, until sourceopen event triggered 192 | this._pendingSourceBufferInit.push(initSegment); 193 | // make sure that this InitSegment is in the front of pending segments queue 194 | this._pendingSegments[initSegment.type].push(initSegment); 195 | return; 196 | } 197 | 198 | let is = initSegment; 199 | let mimeType = `${is.container}`; 200 | if (is.codec && is.codec.length > 0) { 201 | mimeType += `;codecs=${is.codec}`; 202 | } 203 | 204 | let firstInitSegment = false; 205 | 206 | Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType); 207 | this._lastInitSegments[is.type] = is; 208 | 209 | if (mimeType !== this._mimeTypes[is.type]) { 210 | if (!this._mimeTypes[is.type]) { // empty, first chance create sourcebuffer 211 | firstInitSegment = true; 212 | try { 213 | let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType); 214 | sb.addEventListener('error', this.e.onSourceBufferError); 215 | sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd); 216 | } catch (error) { 217 | Log.e(this.TAG, error.message); 218 | this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message}); 219 | return; 220 | } 221 | } else { 222 | Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`); 223 | } 224 | this._mimeTypes[is.type] = mimeType; 225 | } 226 | 227 | if (!deferred) { 228 | // deferred means this InitSegment has been pushed to pendingSegments queue 229 | this._pendingSegments[is.type].push(is); 230 | } 231 | if (!firstInitSegment) { // append immediately only if init segment in subsequence 232 | if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) { 233 | this._doAppendSegments(); 234 | } 235 | } 236 | if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) { 237 | // 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN 238 | // Manually correct MediaSource.duration to make progress bar seekable, and report right duration 239 | this._requireSetMediaDuration = true; 240 | this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds 241 | this._updateMediaSourceDuration(); 242 | } 243 | } 244 | 245 | appendMediaSegment(mediaSegment) { 246 | Log.d(this.TAG, "appendMediaSegment", mediaSegment); 247 | let ms = mediaSegment; 248 | this._pendingSegments[ms.type].push(ms); 249 | 250 | if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) { 251 | this._doCleanupSourceBuffer(); 252 | } 253 | 254 | let sb = this._sourceBuffers[ms.type]; 255 | if (sb && !sb.updating && !this._hasPendingRemoveRanges()) { 256 | this._doAppendSegments(); 257 | } 258 | } 259 | 260 | endOfStream() { 261 | let ms = this._mediaSource; 262 | let sb = this._sourceBuffers; 263 | if (!ms || ms.readyState !== 'open') { 264 | if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) { 265 | // If MediaSource hasn't turned into open state, and there're pending segments 266 | // Mark pending endOfStream, defer call until all pending segments appended complete 267 | this._hasPendingEos = true; 268 | } 269 | return; 270 | } 271 | if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) { 272 | // If any sourcebuffer is updating, defer endOfStream operation 273 | // See _onSourceBufferUpdateEnd() 274 | this._hasPendingEos = true; 275 | } else { 276 | this._hasPendingEos = false; 277 | // Notify media data loading complete 278 | // This is helpful for correcting total duration to match last media segment 279 | // Otherwise MediaElement's ended event may not be triggered 280 | ms.endOfStream(); 281 | } 282 | } 283 | 284 | _needCleanupSourceBuffer() { 285 | if (!this._config.autoCleanupSourceBuffer) { 286 | return false; 287 | } 288 | 289 | let currentTime = this._mediaElement.currentTime; 290 | 291 | for (let type in this._sourceBuffers) { 292 | let sb = this._sourceBuffers[type]; 293 | if (sb) { 294 | let buffered = sb.buffered; 295 | if (buffered.length >= 1) { 296 | if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) { 297 | return true; 298 | } 299 | } 300 | } 301 | } 302 | 303 | return false; 304 | } 305 | 306 | _doCleanupSourceBuffer() { 307 | let currentTime = this._mediaElement.currentTime; 308 | 309 | for (let type in this._sourceBuffers) { 310 | let sb = this._sourceBuffers[type]; 311 | if (sb) { 312 | let buffered = sb.buffered; 313 | let doRemove = false; 314 | 315 | for (let i = 0; i < buffered.length; i++) { 316 | let start = buffered.start(i); 317 | let end = buffered.end(i); 318 | 319 | if (start <= currentTime && currentTime < end + 3) { // padding 3 seconds 320 | if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) { 321 | doRemove = true; 322 | let removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration; 323 | this._pendingRemoveRanges[type].push({start: start, end: removeEnd}); 324 | } 325 | } else if (end < currentTime) { 326 | doRemove = true; 327 | this._pendingRemoveRanges[type].push({start: start, end: end}); 328 | } 329 | } 330 | 331 | if (doRemove && !sb.updating) { 332 | this._doRemoveRanges(); 333 | } 334 | } 335 | } 336 | } 337 | 338 | _updateMediaSourceDuration() { 339 | let sb = this._sourceBuffers; 340 | if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') { 341 | return; 342 | } 343 | if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) { 344 | return; 345 | } 346 | 347 | let current = this._mediaSource.duration; 348 | let target = this._pendingMediaDuration; 349 | 350 | if (target > 0 && (isNaN(current) || target > current)) { 351 | Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`); 352 | this._mediaSource.duration = target; 353 | } 354 | 355 | this._requireSetMediaDuration = false; 356 | this._pendingMediaDuration = 0; 357 | } 358 | 359 | _doRemoveRanges() { 360 | for (let type in this._pendingRemoveRanges) { 361 | if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { 362 | continue; 363 | } 364 | let sb = this._sourceBuffers[type]; 365 | let ranges = this._pendingRemoveRanges[type]; 366 | while (ranges.length && !sb.updating) { 367 | let range = ranges.shift(); 368 | sb.remove(range.start, range.end); 369 | } 370 | } 371 | } 372 | 373 | _doAppendSegments() { 374 | let pendingSegments = this._pendingSegments; 375 | 376 | for (let type in pendingSegments) { 377 | if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { 378 | continue; 379 | } 380 | 381 | if (pendingSegments[type].length > 0) { 382 | let segment = pendingSegments[type].shift(); 383 | 384 | if (segment.timestampOffset) { 385 | // For MPEG audio stream in MSE, if unbuffered-seeking occurred 386 | // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. 387 | let currentOffset = this._sourceBuffers[type].timestampOffset; 388 | let targetOffset = segment.timestampOffset / 1000; // in seconds 389 | 390 | let delta = Math.abs(currentOffset - targetOffset); 391 | if (delta > 0.1) { // If time delta > 100ms 392 | Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`); 393 | this._sourceBuffers[type].timestampOffset = targetOffset; 394 | } 395 | delete segment.timestampOffset; 396 | } 397 | 398 | if (!segment.data || segment.data.byteLength === 0) { 399 | // Ignore empty buffer 400 | continue; 401 | } 402 | 403 | try { 404 | this._sourceBuffers[type].appendBuffer(segment.data); 405 | this._isBufferFull = false; 406 | if (type === 'video' && segment.hasOwnProperty('info')) { 407 | this._idrList.appendArray(segment.info.syncPoints); 408 | } 409 | } catch (error) { 410 | this._pendingSegments[type].unshift(segment); 411 | if (error.code === 22) { // QuotaExceededError 412 | /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full 413 | * Currently we can only do lazy-load to avoid SourceBuffer become scattered. 414 | * SourceBuffer eviction policy may be changed in future version of FireFox. 415 | * 416 | * Related issues: 417 | * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885 418 | * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023 419 | */ 420 | 421 | // report buffer full, abort network IO 422 | if (!this._isBufferFull) { 423 | this._emitter.emit(MSEEvents.BUFFER_FULL); 424 | } 425 | this._isBufferFull = true; 426 | } else { 427 | Log.e(this.TAG, error.message); 428 | this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message}); 429 | } 430 | } 431 | } 432 | } 433 | } 434 | 435 | _onSourceOpen() { 436 | Log.v(this.TAG, 'MediaSource onSourceOpen'); 437 | this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); 438 | // deferred sourcebuffer creation / initialization 439 | if (this._pendingSourceBufferInit.length > 0) { 440 | let pendings = this._pendingSourceBufferInit; 441 | while (pendings.length) { 442 | let segment = pendings.shift(); 443 | this.appendInitSegment(segment, true); 444 | } 445 | } 446 | // there may be some pending media segments, append them 447 | if (this._hasPendingSegments()) { 448 | this._doAppendSegments(); 449 | } 450 | this._emitter.emit(MSEEvents.SOURCE_OPEN); 451 | } 452 | 453 | _onSourceEnded() { 454 | // fired on endOfStream 455 | Log.v(this.TAG, 'MediaSource onSourceEnded'); 456 | } 457 | 458 | _onSourceClose() { 459 | // fired on detaching from media element 460 | Log.v(this.TAG, 'MediaSource onSourceClose'); 461 | if (this._mediaSource && this.e != null) { 462 | this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); 463 | this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded); 464 | this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose); 465 | } 466 | } 467 | 468 | _hasPendingSegments() { 469 | let ps = this._pendingSegments; 470 | return ps.video.length > 0 || ps.audio.length > 0; 471 | } 472 | 473 | _hasPendingRemoveRanges() { 474 | let prr = this._pendingRemoveRanges; 475 | return prr.video.length > 0 || prr.audio.length > 0; 476 | } 477 | 478 | _onSourceBufferUpdateEnd() { 479 | if (this._requireSetMediaDuration) { 480 | this._updateMediaSourceDuration(); 481 | } else if (this._hasPendingRemoveRanges()) { 482 | this._doRemoveRanges(); 483 | } else if (this._hasPendingSegments()) { 484 | this._doAppendSegments(); 485 | } else if (this._hasPendingEos) { 486 | this.endOfStream(); 487 | } 488 | this._emitter.emit(MSEEvents.UPDATE_END); 489 | } 490 | 491 | _onSourceBufferError(e) { 492 | Log.e(this.TAG, `SourceBuffer Error: ${e}`); 493 | // this error might not always be fatal, just ignore it 494 | } 495 | 496 | } 497 | 498 | export default MSEController; 499 | -------------------------------------------------------------------------------- /src/utils/utf8-conv.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * This file is derived from C++ project libWinTF8 (https://github.com/m13253/libWinTF8) 5 | * @author zheng qian 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | export function checkContinuation(uint8array, start, checkLength) { 21 | let array = uint8array; 22 | if (start + checkLength < array.length) { 23 | while (checkLength--) { 24 | if ((array[++start] & 0xC0) !== 0x80) 25 | return false; 26 | } 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | export function decodeUTF8(uint8array) { 34 | let out = []; 35 | let input = uint8array; 36 | let i = 0; 37 | let length = uint8array.length; 38 | 39 | while (i < length) { 40 | if (input[i] < 0x80) { 41 | out.push(String.fromCharCode(input[i])); 42 | ++i; 43 | continue; 44 | } else if (input[i] < 0xC0) { 45 | // fallthrough 46 | } else if (input[i] < 0xE0) { 47 | if (checkContinuation(input, i, 1)) { 48 | let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F); 49 | if (ucs4 >= 0x80) { 50 | out.push(String.fromCharCode(ucs4 & 0xFFFF)); 51 | i += 2; 52 | continue; 53 | } 54 | } 55 | } else if (input[i] < 0xF0) { 56 | if (checkContinuation(input, i, 2)) { 57 | let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F; 58 | if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) { 59 | out.push(String.fromCharCode(ucs4 & 0xFFFF)); 60 | i += 3; 61 | continue; 62 | } 63 | } 64 | } else if (input[i] < 0xF8) { 65 | if (checkContinuation(input, i, 3)) { 66 | let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12 67 | | (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F); 68 | if (ucs4 > 0x10000 && ucs4 < 0x110000) { 69 | ucs4 -= 0x10000; 70 | out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800)); 71 | out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00)); 72 | i += 4; 73 | continue; 74 | } 75 | } 76 | } 77 | out.push(String.fromCharCode(0xFFFD)); 78 | ++i; 79 | } 80 | 81 | return out.join(''); 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Copyright (C) 2023 itNOX. All Rights Reserved. 5 | * 6 | * @author Michael Balen 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * 20 | */ 21 | 22 | /** 23 | * concat two Uint8Array 24 | * @param {Uint8Array} bufs 25 | * @returns {Uint8Array} 26 | */ 27 | 28 | export function _concatArrayBuffers(...bufs){ 29 | const result = new Uint8Array(bufs.reduce((totalSize, buf)=>totalSize+buf.byteLength,0)); 30 | bufs.reduce((offset, buf)=>{ 31 | result.set(buf,offset) 32 | return offset+buf.byteLength 33 | },0) 34 | 35 | return result; 36 | } 37 | 38 | /** 39 | * 40 | * @param {String} str 41 | * @returns {*[]} 42 | * @private 43 | */ 44 | export function _stringToByteArray(str) { 45 | const bytes = []; 46 | 47 | for(let i = 0; i < str.length; i++) { 48 | const char = str.charCodeAt(i); 49 | if(char > 0xFF) { 50 | bytes.push(char >>> 8); 51 | } 52 | 53 | bytes.push(char & 0xFF); 54 | } 55 | return bytes; 56 | } 57 | 58 | /** 59 | * convert Float64 to 8 byteArray 60 | * @param {Number} num 61 | * @returns {*[]} 62 | * @private 63 | */ 64 | export function _numberToByteArray(num) { 65 | const buffer = new ArrayBuffer(8); 66 | new DataView(buffer).setFloat64(0, num, false); 67 | return [].slice.call(new Uint8Array(buffer)); 68 | } 69 | 70 | /** 71 | * convert 8 byte byteArray to Float64 72 | * @param {byte[]} ba 73 | * @returns {number} 74 | * @private 75 | */ 76 | export function _byteArrayToNumber(ba){ 77 | let buf = new ArrayBuffer(ba.length); 78 | let view = new DataView(buf); 79 | 80 | ba.forEach(function (b, i) { 81 | view.setUint8(i, b); 82 | }); 83 | 84 | return view.getFloat64(0); 85 | } 86 | 87 | /** 88 | * convert byteArray to string 89 | * @param {byte[]} ba 90 | * @returns {string} 91 | * @private 92 | */ 93 | export function _byteArrayToString(ba){ 94 | let ret = ""; 95 | 96 | for(let i = 0; i < ba.length; i++){ 97 | ret += String.fromCharCode(ba[i]); 98 | } 99 | 100 | return ret; 101 | } 102 | 103 | export const defaultConfig = { 104 | enableStashBuffer: true, 105 | stashInitialSize: undefined, 106 | 107 | isLive: true, 108 | 109 | autoCleanupSourceBuffer: true, 110 | autoCleanupMaxBackwardDuration: 3 * 60, 111 | autoCleanupMinBackwardDuration: 2 * 60, 112 | 113 | statisticsInfoReportInterval: 600, 114 | 115 | fixAudioTimestampGap: true, 116 | 117 | headers: undefined 118 | }; 119 | 120 | 121 | export const TransmuxingEvents = { 122 | IO_ERROR: 'io_error', 123 | DEMUX_ERROR: 'demux_error', 124 | INIT_SEGMENT: 'init_segment', 125 | MEDIA_SEGMENT: 'media_segment', 126 | LOADING_COMPLETE: 'loading_complete', 127 | RECOVERED_EARLY_EOF: 'recovered_early_eof', 128 | MEDIA_INFO: 'media_info', 129 | METADATA_ARRIVED: 'metadata_arrived', 130 | SCRIPTDATA_ARRIVED: 'scriptdata_arrived', 131 | STATISTICS_INFO: 'statistics_info', 132 | RECOMMEND_SEEKPOINT: 'recommend_seekpoint' 133 | }; 134 | 135 | export const DemuxErrors = { 136 | OK: 'OK', 137 | FORMAT_ERROR: 'FormatError', 138 | FORMAT_UNSUPPORTED: 'FormatUnsupported', 139 | CODEC_UNSUPPORTED: 'CodecUnsupported' 140 | }; 141 | 142 | export const MSEEvents = { 143 | ERROR: 'error', 144 | SOURCE_OPEN: 'source_open', 145 | UPDATE_END: 'update_end', 146 | BUFFER_FULL: 'buffer_full' 147 | }; 148 | 149 | export const PlayerEvents = { 150 | ERROR: 'error', 151 | LOADING_COMPLETE: 'loading_complete', 152 | RECOVERED_EARLY_EOF: 'recovered_early_eof', 153 | MEDIA_INFO: 'media_info', 154 | METADATA_ARRIVED: 'metadata_arrived', 155 | SCRIPTDATA_ARRIVED: 'scriptdata_arrived', 156 | STATISTICS_INFO: 'statistics_info' 157 | }; 158 | 159 | export const ErrorTypes = { 160 | NETWORK_ERROR: 'NetworkError', 161 | MEDIA_ERROR: 'MediaError', 162 | OTHER_ERROR: 'OtherError' 163 | }; 164 | 165 | export const LoaderErrors = { 166 | OK: 'OK', 167 | EXCEPTION: 'Exception', 168 | HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid', 169 | CONNECTING_TIMEOUT: 'ConnectingTimeout', 170 | EARLY_EOF: 'EarlyEof', 171 | UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof' 172 | }; 173 | 174 | export const ErrorDetails = { 175 | NETWORK_EXCEPTION: LoaderErrors.EXCEPTION, 176 | NETWORK_STATUS_CODE_INVALID: LoaderErrors.HTTP_STATUS_CODE_INVALID, 177 | NETWORK_TIMEOUT: LoaderErrors.CONNECTING_TIMEOUT, 178 | NETWORK_UNRECOVERABLE_EARLY_EOF: LoaderErrors.UNRECOVERABLE_EARLY_EOF, 179 | 180 | MEDIA_MSE_ERROR: 'MediaMSEError', 181 | 182 | MEDIA_FORMAT_ERROR: DemuxErrors.FORMAT_ERROR, 183 | MEDIA_FORMAT_UNSUPPORTED: DemuxErrors.FORMAT_UNSUPPORTED, 184 | MEDIA_CODEC_UNSUPPORTED: DemuxErrors.CODEC_UNSUPPORTED 185 | }; 186 | -------------------------------------------------------------------------------- /src/webrtmp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "./utils/logger"; 22 | import MSEController from "./utils/mse-controller"; 23 | import {defaultConfig, ErrorDetails, ErrorTypes, MSEEvents, PlayerEvents, TransmuxingEvents} from "./utils/utils"; 24 | import EventEmitter from "./utils/event_emitter"; 25 | import WebRTMP_Controller from "./wss/webrtmp.controller"; 26 | 27 | /** 28 | * the main class for webrtmp. Handles the remuxer result 29 | */ 30 | export class WebRTMP{ 31 | TAG = 'WebRTMP'; 32 | 33 | /** 34 | * 35 | * @type {HTMLVideoElement} 36 | * @private 37 | */ 38 | _mediaElement = null; 39 | 40 | constructor() { 41 | this.wss = new WebRTMP_Controller(); 42 | 43 | this._config = defaultConfig 44 | 45 | this.wss.addEventListener("RTMPMessageArrived", (data)=>{ 46 | Log.d(this.TAG,"RTMPMessageArrived", data); 47 | }); 48 | 49 | this.wss.addEventListener("ProtocolControlMessage", (data)=>{ 50 | Log.d(this.TAG,"ProtocolControlMessage", data); 51 | }); 52 | 53 | this.wss.addEventListener("UserControlMessage", (data)=>{ 54 | Log.d(this.TAG,"UserControlMessage", data); 55 | }); 56 | 57 | this.wss.addEventListener("ConnectionLost", ()=>{}); 58 | 59 | this._emitter = new EventEmitter(); 60 | 61 | this.e = { 62 | onvLoadedMetadata: this._onvLoadedMetadata.bind(this), 63 | onvCanPlay: this._onvCanPlay.bind(this), 64 | onvStalled: this._onvStalled.bind(this), 65 | onvProgress: this._onvProgress.bind(this), 66 | onvPlay: this._onvPlay.bind(this), 67 | onvPause: this._onvPause.bind(this), 68 | onAppendInitSegment: this._appendMediaSegment.bind(this), 69 | onAppendMediaSegment: this._appendMediaSegment.bind(this) 70 | }; 71 | } 72 | 73 | _checkAndResumeStuckPlayback(stalled) { 74 | let media = this._mediaElement; 75 | if (stalled || !this._receivedCanPlay || media.readyState < 2) { // HAVE_CURRENT_DATA 76 | let buffered = media.buffered; 77 | if (buffered.length > 0 && media.currentTime < buffered.start(0)) { 78 | Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`); 79 | //this._requestSetTime = true; 80 | this._mediaElement.currentTime = buffered.start(0); 81 | this._mediaElement.removeEventListener('progress', this.e.onvProgress); 82 | } 83 | } else { 84 | // Playback didn't stuck, remove progress event listener 85 | this._mediaElement.removeEventListener('progress', this.e.onvProgress); 86 | } 87 | } 88 | 89 | _onvLoadedMetadata() { 90 | if (this._pendingSeekTime != null) { 91 | this._mediaElement.currentTime = this._pendingSeekTime; 92 | this._pendingSeekTime = null; 93 | } 94 | } 95 | 96 | _onvCanPlay(e) { 97 | Log.d(this.TAG, "onvCanPlay", e); 98 | this._mediaElement.play().then(()=>{ 99 | Log.d(this.TAG, "promise play"); 100 | }); 101 | this._receivedCanPlay = true; 102 | this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay); 103 | } 104 | 105 | _onvStalled() { 106 | this._checkAndResumeStuckPlayback(true); 107 | } 108 | 109 | _onvProgress() { 110 | this._checkAndResumeStuckPlayback(); 111 | } 112 | 113 | _onmseBufferFull() { 114 | Log.w(this.TAG, 'MSE SourceBuffer is full'); 115 | } 116 | 117 | _onvPlay(e){ 118 | Log.d(this.TAG, "play:", e); 119 | this.pause(false); 120 | } 121 | 122 | _onvPause(e) { 123 | Log.d(this.TAG, "pause", e); 124 | this.pause(true); 125 | } 126 | 127 | destroy() { 128 | Log.w(this.TAG, "destroy webrtmp"); 129 | if (this._mediaElement) { 130 | this.detachMediaElement(); 131 | } 132 | this.e = null; 133 | this._emitter.removeAllListener(); 134 | this._emitter = null; 135 | } 136 | 137 | disconnect(){ 138 | this.wss.disconnect(); 139 | this.wss.removeAllEventListener("RTMPHandshakeDone"); 140 | this.wss.removeAllEventListener("WSSConnectFailed"); 141 | } 142 | 143 | /** 144 | * send play command 145 | * @param {String} streamName 146 | * @returns {Promise} 147 | */ 148 | play(streamName){ 149 | this.wss.play(streamName); 150 | return this._mediaElement.play(); 151 | } 152 | 153 | /** 154 | * Stops loading, same as pause(true) 155 | */ 156 | stopLoad(){ 157 | //this.wss.stop() 158 | this._mediaElement.pause(); 159 | } 160 | 161 | /** 162 | * 163 | * @param {String|null} host 164 | * @param {Number|null} port 165 | * @returns {Promise} 166 | */ 167 | open(host, port){ 168 | return this.wss.open(host, port); 169 | } 170 | 171 | /** 172 | * 173 | * @param {String} appName 174 | * @returns {Promise} 175 | */ 176 | connect(appName){ 177 | return this.wss.connect(appName); 178 | } 179 | 180 | /** 181 | * Pause a rtmp stream 182 | * @param {boolean} enable 183 | */ 184 | pause(enable){ 185 | this.wss.pause(enable); 186 | 187 | if(enable) { 188 | this._mediaElement.pause(); 189 | 190 | } else { 191 | this.kerkDown = 10; 192 | this._mediaElement.play().then(()=>{ 193 | 194 | }); 195 | } 196 | } 197 | 198 | /** 199 | * Detach Mediaelement 200 | */ 201 | detachMediaElement() { 202 | this.wss.removeAllEventListener(TransmuxingEvents.INIT_SEGMENT); 203 | this.wss.removeAllEventListener(TransmuxingEvents.MEDIA_SEGMENT); 204 | 205 | if (this._mediaElement) { 206 | this._msectl.detachMediaElement(); 207 | this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata); 208 | this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay); 209 | this._mediaElement.removeEventListener('stalled', this.e.onvStalled); 210 | this._mediaElement.removeEventListener('progress', this.e.onvProgress); 211 | this._mediaElement.removeEventListener('play', this.e.onvPlay); 212 | this._mediaElement.removeEventListener('pause', this.e.onvPause); 213 | this._mediaElement = null; 214 | } 215 | 216 | if (this._msectl) { 217 | this._msectl.destroy(); 218 | this._msectl = null; 219 | } 220 | 221 | this.disconnect(); 222 | } 223 | 224 | /** 225 | * Attach MediaElement 226 | * @param {HTMLVideoElement} mediaElement 227 | */ 228 | attachMediaElement(mediaElement) { 229 | this._mediaElement = mediaElement; 230 | mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata); 231 | mediaElement.addEventListener('canplay', this.e.onvCanPlay); 232 | mediaElement.addEventListener('stalled', this.e.onvStalled); 233 | mediaElement.addEventListener('progress', this.e.onvProgress); 234 | mediaElement.addEventListener('play', this.e.onvPlay); 235 | mediaElement.addEventListener('pause', this.e.onvPause); 236 | 237 | this._msectl = new MSEController(defaultConfig); 238 | 239 | //this._msectl.on(MSEEvents.UPDATE_END, this._onmseUpdateEnd.bind(this)); 240 | this._msectl.on(MSEEvents.BUFFER_FULL, this._onmseBufferFull.bind(this)); 241 | 242 | this._msectl.on(MSEEvents.ERROR, (info) => { 243 | this._emitter.emit(PlayerEvents.ERROR, 244 | ErrorTypes.MEDIA_ERROR, 245 | ErrorDetails.MEDIA_MSE_ERROR, 246 | info 247 | ); 248 | }); 249 | 250 | this.wss.addEventListener(TransmuxingEvents.INIT_SEGMENT, this._appendInitSegment.bind(this), true); 251 | this.wss.addEventListener(TransmuxingEvents.MEDIA_SEGMENT, this._appendMediaSegment.bind(this), true); 252 | 253 | this._msectl.attachMediaElement(mediaElement); 254 | } 255 | 256 | /** 257 | * Append Init Segment to MSE 258 | * @param data 259 | * @private 260 | */ 261 | _appendInitSegment(data){ 262 | Log.i(this.TAG, TransmuxingEvents.INIT_SEGMENT, data[0], data[1]); 263 | this._msectl.appendInitSegment(data[1]); 264 | } 265 | 266 | /** 267 | * Append Media Segment to MSE 268 | * @param data 269 | * @private 270 | */ 271 | _appendMediaSegment(data){ 272 | Log.t(this.TAG, TransmuxingEvents.MEDIA_SEGMENT, data[0], data[1]); 273 | this._msectl.appendMediaSegment(data[1]); 274 | if(this.kerkDown) { 275 | this.kerkDown--; 276 | this._mediaElement.currentTime = 2000000000; 277 | 278 | if(!this.kerkDown) Log.d(this.TAG, "kerkdown reached"); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/wss/WSSConnectionManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import Log from "../utils/logger"; 22 | 23 | class WSSConnectionManager{ 24 | TAG = "WSSConnectionManager"; 25 | host; 26 | port; 27 | wss; 28 | 29 | /** 30 | * Open WSS connection 31 | * @param {String} host 32 | * @param {Number} port 33 | * @param callback 34 | */ 35 | open(host, port, callback){ 36 | this.host = host; 37 | Log.v(this.TAG, "connecting to: " + host + ":" + port); 38 | this.wss = new WebSocket("wss://" + host + ":" + port + "/"); 39 | 40 | this.wss.binaryType = "arraybuffer"; 41 | 42 | this.wss.onopen = (e)=>{ 43 | Log.v(this.TAG, e); 44 | callback(true); 45 | } 46 | 47 | this.wss.onclose = (e)=>{ 48 | Log.w(this.TAG, e); 49 | postMessage(["ConnectionLost"]); 50 | } 51 | 52 | this.wss.onerror = (e)=>{ 53 | Log.e(this.TAG, e); 54 | callback(false); 55 | } 56 | } 57 | 58 | /** 59 | * register a callback for messages 60 | * @param cb 61 | */ 62 | registerMessageHandler(cb){ 63 | this.wss.onmessage = cb; 64 | } 65 | 66 | /** 67 | * returns the WebSocket 68 | * @returns {WebSocket} 69 | */ 70 | getSocket(){ 71 | return this.wss; 72 | } 73 | 74 | getHost(){ 75 | return this.host; 76 | } 77 | 78 | /** 79 | * close Websocket 80 | */ 81 | close(){ 82 | this.wss.close(); 83 | } 84 | } 85 | 86 | export default WSSConnectionManager; 87 | -------------------------------------------------------------------------------- /src/wss/connection.worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import WSSConnectionManager from "./WSSConnectionManager"; 22 | import RTMPHandshake from "../rtmp/RTMPHandshake"; 23 | import RTMPMessageHandler from "../rtmp/RTMPMessageHandler"; 24 | import Log from "../utils/logger"; 25 | 26 | const TAG = "WebRTMP Worker"; 27 | 28 | let port; 29 | let host; 30 | let message_handler; 31 | Log.LEVEL = Log.DEBUG; 32 | 33 | const wss_manager = new WSSConnectionManager(); 34 | 35 | self.addEventListener('message', function(e) { 36 | let data = e.data; 37 | 38 | Log.d(TAG, "CMD: " + data.cmd); 39 | 40 | switch(data.cmd) { 41 | case "open": // connect WebSocket 42 | host = data.host; 43 | port = data.port; 44 | 45 | wss_manager.open(host, port, (success)=>{ 46 | Log.v(TAG, "open: " + host + ":" +port); 47 | if(success){ 48 | Log.v(TAG, "WSSConnected"); 49 | postMessage(["WSSConnected"]); 50 | 51 | const handshake = new RTMPHandshake(wss_manager.getSocket()); 52 | 53 | handshake.onHandshakeDone = (success)=>{ 54 | if(success){ 55 | message_handler = new RTMPMessageHandler(wss_manager.getSocket()); 56 | 57 | Log.d(TAG, "connect to RTMPManager"); 58 | 59 | wss_manager.registerMessageHandler((e)=> { 60 | message_handler.parseChunk(new Uint8Array(e.data)); 61 | }); 62 | 63 | postMessage(["RTMPHandshakeDone"]); 64 | 65 | } else { 66 | Log.e(TAG, "Handshake failed"); 67 | postMessage(["RTMPHandshakeFailed"]); 68 | } 69 | }; 70 | 71 | handshake.do(); 72 | 73 | } else { 74 | Log.v(this.TAG, "WSSConnectFailed"); 75 | postMessage(["WSSConnectFailed"]); 76 | } 77 | }); 78 | break; 79 | 80 | case "connect": // RTMP Connect Application 81 | if(!message_handler) { 82 | Log.e(this.TAG, "RTMP not connected"); 83 | break; 84 | } 85 | message_handler.connect(makeDefaultConnectionParams(data.appName)); 86 | break; 87 | 88 | case "play": 89 | if(!message_handler) { 90 | Log.e(this.TAG, "RTMP not connected"); 91 | break; 92 | } 93 | message_handler.play(data.streamName); 94 | break; 95 | 96 | case "stop": 97 | if(!message_handler) { 98 | Log.e(this.TAG, "RTMP not connected"); 99 | break; 100 | } 101 | message_handler.stop(); 102 | break; 103 | 104 | case "pause": 105 | if(!message_handler) { 106 | Log.e(this.TAG, "RTMP not connected"); 107 | break; 108 | } 109 | message_handler.pause(data.enable); 110 | break; 111 | 112 | case "disconnect": 113 | if(message_handler) { 114 | message_handler.destroy(); 115 | } 116 | wss_manager.close(); 117 | break; 118 | 119 | case "loglevels": 120 | Log.d(TAG, "setting loglevels", data.loglevels); 121 | Log.loglevels = data.loglevels; 122 | break; 123 | 124 | default: 125 | Log.w(TAG, "Unknown CMD: " + data.cmd); 126 | break; 127 | } 128 | 129 | }, false); 130 | 131 | function makeDefaultConnectionParams(application){ 132 | return { 133 | "app": application, 134 | "flashVer": "WebRTMP 0,0,1", 135 | "tcUrl": "rtmp://" + host + ":1935/" + application, 136 | "fpad": false, 137 | "capabilities": 15, 138 | "audioCodecs": 0x0400, // AAC 139 | "videoCodecs": 0x0080, // H264 140 | "videoFunction": 0 // Seek false 141 | }; 142 | } 143 | 144 | postMessage(["Started"]); 145 | 146 | -------------------------------------------------------------------------------- /src/wss/webrtmp.controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (C) 2023 itNOX. All Rights Reserved. 4 | * 5 | * @author Michael Balen 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * 19 | */ 20 | 21 | import EventEmitter from "../utils/event_emitter"; 22 | import Log from "../utils/logger"; 23 | import Worker from "./connection.worker.js"; 24 | 25 | 26 | /** 27 | * Class which handles the Websocket 28 | */ 29 | class WebRTMP_Controller { 30 | TAG = "WebRTMP_Controller"; 31 | host = document.location.host; 32 | port = 9001; 33 | WSSReconnect = false; 34 | isConnected = false; 35 | 36 | WebRTMPWorker = new Worker(); 37 | 38 | constructor() { 39 | Log.loglevels = { 40 | "RTMPMessage": Log.ERROR, 41 | "RTMPMessageHandler": Log.WARN, 42 | "RTMPMediaMessageHandler": Log.ERROR, 43 | "ChunkParser": Log.WARN, 44 | "RTMPHandshake": Log.ERROR, 45 | "Chunk": Log.OFF, 46 | "MP4Remuxer": Log.ERROR, 47 | "Transmuxer": Log.WARN, 48 | "EventEmitter": Log.DEBUG, 49 | "MSEController": Log.INFO, 50 | "WebRTMP": Log.DEBUG, 51 | "WebRTMP_Controller": Log.WARN, 52 | "WebRTMP Worker": Log.WARN, 53 | "AMF": Log.WARN, 54 | "WSSConnectionManager": Log.DEBUG 55 | }; 56 | 57 | this._emitter = new EventEmitter(); 58 | 59 | this.WebRTMPWorker.addEventListener("message", (evt)=>{ 60 | this.WorkerListener(evt); 61 | }); 62 | } 63 | 64 | /** 65 | * Opens a WSS Connection 66 | * @param {String|null} host 67 | * @param {Number|null} port 68 | */ 69 | open(host, port){ 70 | return new Promise((resolve, reject)=>{ 71 | if(this.isConnected) return reject("Already Connected. Please disconnect first"); 72 | this._emitter.waitForEvent("RTMPHandshakeDone", resolve); 73 | this._emitter.waitForEvent("WSSConnectFailed", reject); 74 | 75 | if(host) this.host = host; 76 | if(port) this.port = port; 77 | 78 | this.WebRTMPWorker.postMessage({cmd: "open", host: this.host, port: this.port}); 79 | }) 80 | } 81 | 82 | /** 83 | * Websocket disconnect 84 | */ 85 | disconnect() { 86 | this.WSSReconnect = false; 87 | this.WebRTMPWorker.postMessage({cmd: "disconnect"}); 88 | } 89 | 90 | /** 91 | * RTMP connect application 92 | * @param {String} appName 93 | */ 94 | connect(appName){ 95 | return new Promise((resolve, reject)=>{ 96 | this._emitter.waitForEvent("RTMPStreamCreated", resolve); 97 | this.WebRTMPWorker.postMessage({cmd: "connect", appName: appName}); 98 | }) 99 | 100 | } 101 | 102 | /** 103 | * RTMP play streamname 104 | * @param {String} streamName 105 | */ 106 | play(streamName){ 107 | this.WebRTMPWorker.postMessage({cmd: "play", streamName: streamName}); 108 | } 109 | 110 | /** 111 | * RTMP stop 112 | */ 113 | stop(){ 114 | this.WebRTMPWorker.postMessage({cmd: "stop"}); 115 | } 116 | 117 | /** 118 | * Pause a video, RTMP Connection will also paused 119 | * @param {boolean} enable - Enable or disable pause mode 120 | */ 121 | pause(enable){ 122 | this.WebRTMPWorker.postMessage({cmd: "pause", enable: enable}); 123 | } 124 | 125 | 126 | /** 127 | * add Eventlistener 128 | * @param type 129 | * @param listener 130 | * @param {boolean} modal - Register only one Event, if exists overwrite 131 | */ 132 | addEventListener(type, listener, modal){ 133 | this._emitter.addEventListener(type, listener, modal); 134 | } 135 | 136 | /** 137 | * Remove Eventlistner 138 | * @param {String} type - Event name 139 | * @param {Function} listener - callback when event occurs 140 | */ 141 | removeEventListener(type, listener){ 142 | this._emitter.removeEventListener(type, listener); 143 | } 144 | 145 | /** 146 | * Remove All registered Listener 147 | * @param type 148 | */ 149 | removeAllEventListener(type){ 150 | this._emitter.removeAllEventListener(type); 151 | } 152 | 153 | 154 | /** 155 | * 156 | * @param {MessageEvent} evt 157 | * @constructor 158 | */ 159 | WorkerListener(evt){ 160 | // Message.data wieder zum Event machen 161 | const data = evt.data; 162 | 163 | switch(data[0]){ 164 | case "ConnectionLost": 165 | this._emitter.emit("ConnectionLost"); 166 | Log.d(this.TAG, "Event ConnectionLost"); 167 | 168 | this.isConnected = false; 169 | 170 | if(this.WSSReconnect) { 171 | Log.w(this.TAG,"[ WorkerListener ] Reconnect timed"); 172 | 173 | window.setTimeout(()=>{ 174 | Log.w(this.TAG, "timed Reconnect"); 175 | this.open(this.host, this.port); 176 | }, 1000) 177 | } 178 | 179 | break; 180 | 181 | case "Connected": 182 | Log.d(this.TAG, "Event Connected"); 183 | this._emitter.emit("Connected"); 184 | this.isConnected = true; 185 | break; 186 | 187 | case "Started": 188 | this.WebRTMPWorker.postMessage({ 189 | cmd: "loglevels", 190 | loglevels: this.loglevels 191 | }); 192 | break; 193 | 194 | default: 195 | Log.i(this.TAG, data[0], data.slice(1)); 196 | this._emitter.emit(data[0], data.slice(1)); 197 | break; 198 | } 199 | } 200 | } 201 | 202 | export default WebRTMP_Controller; 203 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: "development", 5 | 6 | entry: "./src/index.js", 7 | output: { 8 | path: path.resolve(__dirname, "dist"), 9 | filename: "webrtmp.js", 10 | library: "webrtmpjs", 11 | libraryTarget: "umd", 12 | globalObject: "this", 13 | //chunkFilename: "[name].js" 14 | }, 15 | 16 | devtool: 'source-map', 17 | 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.worker\.js$/i, 22 | loader: "worker-loader", 23 | options: { 24 | inline: "no-fallback", 25 | }, 26 | }, 27 | ], 28 | }, 29 | 30 | 31 | optimization: { 32 | concatenateModules: true, 33 | usedExports: true, 34 | providedExports: true, 35 | chunkIds: "deterministic" // To keep filename consistent between different modes (for example building only) 36 | } 37 | }; 38 | --------------------------------------------------------------------------------