├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------