Migrate PSI parsing
Move the code that parses PMTs and PATs from segment parser into the re-organized transmuxing module. Add tests.
Showing
2 changed files
with
432 additions
and
3 deletions
... | @@ -7,7 +7,187 @@ | ... | @@ -7,7 +7,187 @@ |
7 | (function(window, videojs, undefined) { | 7 | (function(window, videojs, undefined) { |
8 | 'use strict'; | 8 | 'use strict'; |
9 | 9 | ||
10 | var Transmuxer = function() { | 10 | var PacketStream, ParseStream, Transmuxer, MP2T_PACKET_LENGTH; |
11 | |||
12 | MP2T_PACKET_LENGTH = 188; // bytes | ||
13 | |||
14 | /** | ||
15 | * Splits an incoming stream of binary data into MP2T packets. | ||
16 | */ | ||
17 | PacketStream = function() { | ||
18 | var | ||
19 | buffer = new Uint8Array(MP2T_PACKET_LENGTH), | ||
20 | end = 0; | ||
21 | |||
22 | PacketStream.prototype.init.call(this); | ||
23 | |||
24 | /** | ||
25 | * Deliver new bytes to the stream. | ||
26 | */ | ||
27 | this.push = function(bytes) { | ||
28 | var remaining, i; | ||
29 | |||
30 | // clear out any partial packets in the buffer | ||
31 | if (end > 0) { | ||
32 | remaining = MP2T_PACKET_LENGTH - end; | ||
33 | buffer.set(bytes.subarray(0, remaining), end); | ||
34 | |||
35 | // we still didn't write out a complete packet | ||
36 | if (bytes.byteLength < remaining) { | ||
37 | end += bytes.byteLength; | ||
38 | return; | ||
39 | } | ||
40 | |||
41 | bytes = bytes.subarray(remaining); | ||
42 | end = 0; | ||
43 | this.trigger('data', buffer); | ||
44 | } | ||
45 | |||
46 | // if less than a single packet is available, buffer it up for later | ||
47 | if (bytes.byteLength < MP2T_PACKET_LENGTH) { | ||
48 | buffer.set(bytes.subarray(i), end); | ||
49 | end += bytes.byteLength; | ||
50 | return; | ||
51 | } | ||
52 | // parse out all the completed packets | ||
53 | i = 0; | ||
54 | do { | ||
55 | this.trigger('data', bytes.subarray(i, i + MP2T_PACKET_LENGTH)); | ||
56 | i += MP2T_PACKET_LENGTH; | ||
57 | remaining = bytes.byteLength - i; | ||
58 | } while (i < bytes.byteLength && remaining >= MP2T_PACKET_LENGTH); | ||
59 | // buffer any partial packets left over | ||
60 | if (remaining > 0) { | ||
61 | buffer.set(bytes.subarray(i)); | ||
62 | end = remaining; | ||
63 | } | ||
64 | }; | ||
65 | }; | ||
66 | PacketStream.prototype = new videojs.Hls.Stream(); | ||
67 | |||
68 | /** | ||
69 | * Accepts an MP2T PacketStream and emits data events with parsed | ||
70 | * forms of the individual packets. | ||
71 | */ | ||
72 | ParseStream = function() { | ||
73 | var parsePsi, parsePat, parsePmt, self; | ||
74 | PacketStream.prototype.init.call(this); | ||
75 | self = this; | ||
76 | |||
77 | this.programMapTable = {}; | ||
78 | |||
79 | parsePsi = function(payload, psi) { | ||
80 | var offset = 0; | ||
81 | |||
82 | // PSI packets may be split into multiple sections and those | ||
83 | // sections may be split into multiple packets. If a PSI | ||
84 | // section starts in this packet, the payload_unit_start_indicator | ||
85 | // will be true and the first byte of the payload will indicate | ||
86 | // the offset from the current position to the start of the | ||
87 | // section. | ||
88 | if (psi.payloadUnitStartIndicator) { | ||
89 | offset += payload[offset] + 1; | ||
90 | } | ||
91 | |||
92 | if (psi.type === 'pat') { | ||
93 | parsePat(payload.subarray(offset), psi); | ||
94 | } else { | ||
95 | parsePmt(payload.subarray(offset), psi); | ||
96 | } | ||
97 | }; | ||
98 | |||
99 | parsePat = function(payload, pat) { | ||
100 | // skip the PSI header and parse the first PMT entry | ||
101 | self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; | ||
102 | pat.pmtPid = self.pmtPid; | ||
103 | }; | ||
104 | |||
105 | /** | ||
106 | * Parse out the relevant fields of a Program Map Table (PMT). | ||
107 | * @param payload {Uint8Array} the PMT-specific portion of an MP2T | ||
108 | * packet. The first byte in this array should be the table_id | ||
109 | * field. | ||
110 | * @param pmt {object} the object that should be decorated with | ||
111 | * fields parsed from the PMT. | ||
112 | */ | ||
113 | parsePmt = function(payload, pmt) { | ||
114 | var tableEnd, programInfoLength, offset; | ||
115 | |||
116 | // PMTs can be sent ahead of the time when they should actually | ||
117 | // take effect. We don't believe this should ever be the case | ||
118 | // for HLS but we'll ignore "forward" PMT declarations if we see | ||
119 | // them. Future PMT declarations have the current_next_indicator | ||
120 | // set to zero. | ||
121 | if (!(payload[5] & 0x01)) { | ||
122 | return; | ||
123 | } | ||
124 | |||
125 | // overwrite any existing program map table | ||
126 | self.programMapTable = {}; | ||
127 | |||
128 | // the mapping table ends right before the 32-bit CRC | ||
129 | tableEnd = payload.byteLength - 4; | ||
130 | // to determine where the table starts, we have to figure out how | ||
131 | // long the program info descriptors are | ||
132 | programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; | ||
133 | |||
134 | // advance the offset to the first entry in the mapping table | ||
135 | offset = 12 + programInfoLength; | ||
136 | while (offset < tableEnd) { | ||
137 | // add an entry that maps the elementary_pid to the stream_type | ||
138 | self.programMapTable[(payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]] = payload[offset]; | ||
139 | |||
140 | // move to the next table entry | ||
141 | // skip past the elementary stream descriptors, if present | ||
142 | offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; | ||
143 | } | ||
144 | }; | ||
145 | |||
146 | /** | ||
147 | * Deliver a new MP2T packet to the stream. | ||
148 | */ | ||
149 | this.push = function(packet) { | ||
150 | var | ||
151 | result = {}, | ||
152 | offset = 4, | ||
153 | stream; | ||
154 | // make sure packet is aligned on a sync byte | ||
155 | if (packet[0] !== 0x47) { | ||
156 | return this.trigger('error', 'mis-aligned packet'); | ||
157 | } | ||
158 | result.payloadUnitStartIndicator = !!(packet[1] & 0x40); | ||
159 | |||
160 | // pid is a 13-bit field starting at the last bit of packet[1] | ||
161 | result.pid = packet[1] & 0x1f; | ||
162 | result.pid <<= 8; | ||
163 | result.pid |= packet[2]; | ||
164 | |||
165 | // if an adaption field is present, its length is specified by | ||
166 | // the fifth byte of the PES header. The adaptation field is | ||
167 | // used to specify some forms of timing and control data that we | ||
168 | // do not currently use. | ||
169 | if (((packet[3] & 0x30) >>> 4) > 0x01) { | ||
170 | offset += packet[offset] + 1; | ||
171 | } | ||
172 | |||
173 | // parse the rest of the packet based on the type | ||
174 | if (result.pid === 0) { | ||
175 | result.type = 'pat'; | ||
176 | parsePsi(packet.subarray(offset), result); | ||
177 | } else if (result.pid === this.pmtPid) { | ||
178 | result.type = 'pmt'; | ||
179 | parsePsi(packet.subarray(offset), result); | ||
180 | } else { | ||
181 | result.stream = this.programMapTable[result.pid]; | ||
182 | result.type = 'pes'; | ||
183 | } | ||
184 | |||
185 | this.trigger('data', result); | ||
186 | }; | ||
187 | }; | ||
188 | ParseStream.prototype = new videojs.Hls.Stream(); | ||
189 | |||
190 | Transmuxer = function() { | ||
11 | Transmuxer.prototype.init.call(this); | 191 | Transmuxer.prototype.init.call(this); |
12 | this.push = function() { | 192 | this.push = function() { |
13 | this.mp4 = new Uint8Array(); | 193 | this.mp4 = new Uint8Array(); |
... | @@ -15,5 +195,13 @@ var Transmuxer = function() { | ... | @@ -15,5 +195,13 @@ var Transmuxer = function() { |
15 | }; | 195 | }; |
16 | Transmuxer.prototype = new videojs.Hls.Stream(); | 196 | Transmuxer.prototype = new videojs.Hls.Stream(); |
17 | 197 | ||
18 | window.videojs.Hls.Transmuxer = Transmuxer; | 198 | window.videojs.mp2t = { |
199 | PAT_PID: 0x0000, | ||
200 | MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, | ||
201 | H264_STREAM_TYPE: 0x1b, | ||
202 | ADTS_STREAM_TYPE: 0x0f, | ||
203 | Transmuxer: Transmuxer, | ||
204 | PacketStream: PacketStream, | ||
205 | ParseStream: ParseStream | ||
206 | }; | ||
19 | })(window, window.videojs); | 207 | })(window, window.videojs); | ... | ... |
... | @@ -21,9 +21,250 @@ | ... | @@ -21,9 +21,250 @@ |
21 | throws(block, [expected], [message]) | 21 | throws(block, [expected], [message]) |
22 | */ | 22 | */ |
23 | var | 23 | var |
24 | Transmuxer = videojs.Hls.Transmuxer, | 24 | PacketStream = videojs.mp2t.PacketStream, |
25 | packetStream, | ||
26 | ParseStream = videojs.mp2t.ParseStream, | ||
27 | parseStream, | ||
28 | Transmuxer = videojs.mp2t.Transmuxer, | ||
25 | transmuxer; | 29 | transmuxer; |
26 | 30 | ||
31 | module('MP2T Packet Stream', { | ||
32 | setup: function() { | ||
33 | packetStream = new PacketStream(); | ||
34 | } | ||
35 | }); | ||
36 | |||
37 | test('empty input does not error', function() { | ||
38 | packetStream.push(new Uint8Array([])); | ||
39 | ok(true, 'did not throw'); | ||
40 | }); | ||
41 | test('parses a generic packet', function() { | ||
42 | var datas = []; | ||
43 | packetStream.on('data', function(event) { | ||
44 | datas.push(event); | ||
45 | }); | ||
46 | packetStream.push(new Uint8Array(188)); | ||
47 | |||
48 | equal(1, datas.length, 'fired one event'); | ||
49 | equal(datas[0].byteLength, 188, 'delivered the packet'); | ||
50 | }); | ||
51 | |||
52 | test('buffers partial packets', function() { | ||
53 | var datas = []; | ||
54 | packetStream.on('data', function(event) { | ||
55 | datas.push(event); | ||
56 | }); | ||
57 | packetStream.push(new Uint8Array(187)); | ||
58 | |||
59 | equal(0, datas.length, 'did not fire an event'); | ||
60 | |||
61 | packetStream.push(new Uint8Array(189)); | ||
62 | equal(2, datas.length, 'fired events'); | ||
63 | equal(188, datas[0].byteLength, 'parsed the first packet'); | ||
64 | equal(188, datas[1].byteLength, 'parsed the second packet'); | ||
65 | }); | ||
66 | |||
67 | test('parses multiple packets delivered at once', function() { | ||
68 | var datas = []; | ||
69 | packetStream.on('data', function(event) { | ||
70 | datas.push(event); | ||
71 | }); | ||
72 | |||
73 | packetStream.push(new Uint8Array(188 * 3)); | ||
74 | equal(3, datas.length, 'fired three events'); | ||
75 | equal(188, datas[0].byteLength, 'parsed the first packet'); | ||
76 | equal(188, datas[1].byteLength, 'parsed the second packet'); | ||
77 | equal(188, datas[2].byteLength, 'parsed the third packet'); | ||
78 | }); | ||
79 | |||
80 | test('buffers extra after multiple packets', function() { | ||
81 | var datas = []; | ||
82 | packetStream.on('data', function(event) { | ||
83 | datas.push(event); | ||
84 | }); | ||
85 | |||
86 | packetStream.push(new Uint8Array(188 * 2 + 10)); | ||
87 | equal(2, datas.length, 'fired two events'); | ||
88 | equal(188, datas[0].byteLength, 'parsed the first packet'); | ||
89 | equal(188, datas[1].byteLength, 'parsed the second packet'); | ||
90 | |||
91 | packetStream.push(new Uint8Array(178)); | ||
92 | equal(3, datas.length, 'fired a final event'); | ||
93 | equal(188, datas[2].length, 'parsed the finel packet'); | ||
94 | }); | ||
95 | |||
96 | module('MP2T Parse Stream', { | ||
97 | setup: function() { | ||
98 | parseStream = new ParseStream(); | ||
99 | } | ||
100 | }); | ||
101 | |||
102 | test('emits an error on an invalid packet', function() { | ||
103 | var errors = []; | ||
104 | parseStream.on('error', function(error) { | ||
105 | errors.push(error); | ||
106 | }); | ||
107 | parseStream.push(new Uint8Array(188)); | ||
108 | |||
109 | equal(1, errors.length, 'emitted an error'); | ||
110 | }); | ||
111 | |||
112 | test('parses generic packet properties', function() { | ||
113 | var packet; | ||
114 | parseStream.on('data', function(data) { | ||
115 | packet = data; | ||
116 | }); | ||
117 | |||
118 | parseStream.push(new Uint8Array([ | ||
119 | 0x47, // sync byte | ||
120 | // tei:0 pusi:1 tp:0 pid:0 0000 0000 0001 tsc:01 afc:10 cc:11 padding: 00 | ||
121 | 0x40, 0x01, 0x6c | ||
122 | ])); | ||
123 | ok(packet.payloadUnitStartIndicator, 'parsed payload_unit_start_indicator'); | ||
124 | ok(packet.pid, 'parsed PID'); | ||
125 | }); | ||
126 | |||
127 | test('parses a data packet with adaptation fields', function() { | ||
128 | var packet; | ||
129 | parseStream.on('data', function(data) { | ||
130 | packet = data; | ||
131 | }); | ||
132 | |||
133 | parseStream.push(new Uint8Array([ | ||
134 | 0x47, // sync byte | ||
135 | // tei:0 pusi:1 tp:0 pid:0 0000 0000 0000 tsc:01 afc:10 cc:11 afl:00 0000 00 stuffing:00 0000 00 pscp:00 0001 padding:0000 | ||
136 | 0x40, 0x00, 0x6c, 0x00, 0x00, 0x10 | ||
137 | ])); | ||
138 | strictEqual(packet.type, 'pat', 'parsed the packet type'); | ||
139 | }); | ||
140 | |||
141 | test('parses a PES packet', function() { | ||
142 | var packet; | ||
143 | parseStream.on('data', function(data) { | ||
144 | packet = data; | ||
145 | }); | ||
146 | |||
147 | // setup a program map table | ||
148 | parseStream.programMapTable = { | ||
149 | 0x0010: videojs.mp2t.H264_STREAM_TYPE | ||
150 | }; | ||
151 | |||
152 | parseStream.push(new Uint8Array([ | ||
153 | 0x47, // sync byte | ||
154 | // tei:0 pusi:1 tp:0 pid:0 0000 0000 0010 tsc:01 afc:01 cc:11 padding:00 | ||
155 | 0x40, 0x02, 0x5c | ||
156 | ])); | ||
157 | strictEqual(packet.type, 'pes', 'parsed a PES packet'); | ||
158 | }); | ||
159 | |||
160 | test('parses packets with variable length adaptation fields and a payload', function() { | ||
161 | var packet; | ||
162 | parseStream.on('data', function(data) { | ||
163 | packet = data; | ||
164 | }); | ||
165 | |||
166 | // setup a program map table | ||
167 | parseStream.programMapTable = { | ||
168 | 0x0010: videojs.mp2t.H264_STREAM_TYPE | ||
169 | }; | ||
170 | |||
171 | parseStream.push(new Uint8Array([ | ||
172 | 0x47, // sync byte | ||
173 | // tei:0 pusi:1 tp:0 pid:0 0000 0000 0010 tsc:01 afc:11 cc:11 afl:00 0000 11 stuffing:00 0000 0000 00 pscp:00 0001 | ||
174 | 0x40, 0x02, 0x7c, 0x0c, 0x00, 0x01 | ||
175 | ])); | ||
176 | strictEqual(packet.type, 'pes', 'parsed a PES packet'); | ||
177 | }); | ||
178 | |||
179 | /* | ||
180 | Packet Header: | ||
181 | | sb | tei pusi tp pid:5 | pid | tsc afc cc | | ||
182 | with af: | ||
183 | | afl | ... | <data> | | ||
184 | without af: | ||
185 | | <data> | | ||
186 | |||
187 | PAT: | ||
188 | | pf? | ... | | ||
189 | | tid | ssi '0' r sl:4 | sl | tsi:8 | | ||
190 | | tsi | r vn cni | sn | lsn | | ||
191 | |||
192 | with program_number == '0': | ||
193 | | pn | pn | r np:5 | np | | ||
194 | otherwise: | ||
195 | | pn | pn | r pmp:5 | pmp | | ||
196 | */ | ||
197 | |||
198 | test('parses the program map table pid from the program association table (PAT)', function() { | ||
199 | var packet; | ||
200 | parseStream.on('data', function(data) { | ||
201 | packet = data; | ||
202 | }); | ||
203 | |||
204 | parseStream.push(new Uint8Array([ | ||
205 | 0x47, // sync byte | ||
206 | // tei:0 pusi:1 tp:0 pid:0 0000 0000 0000 | ||
207 | 0x40, 0x00, | ||
208 | // tsc:01 afc:01 cc:0000 pointer_field:0000 0000 | ||
209 | 0x50, 0x00, | ||
210 | // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0000 0000 | ||
211 | 0x00, 0x00, 0x00, | ||
212 | // tsi:0000 0000 0000 0000 | ||
213 | 0x00, 0x00, | ||
214 | // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000 | ||
215 | 0x01, 0x00, 0x00, | ||
216 | // pn:0000 0000 0000 0001 | ||
217 | 0x00, 0x01, | ||
218 | // r:000 pmp:0 0000 0010 0000 | ||
219 | 0x00, 0x10, | ||
220 | // crc32:0000 0000 0000 0000 0000 0000 0000 0000 | ||
221 | 0x00, 0x00, 0x00, 0x00 | ||
222 | ])); | ||
223 | strictEqual(0x0010, parseStream.pmtPid, 'parsed PMT pid'); | ||
224 | }); | ||
225 | |||
226 | test('parse the elementary streams from a program map table', function() { | ||
227 | var packet; | ||
228 | parseStream.on('data', function(data) { | ||
229 | packet = data; | ||
230 | }); | ||
231 | parseStream.pmtPid = 0x0010; | ||
232 | |||
233 | parseStream.push(new Uint8Array([ | ||
234 | 0x47, // sync byte | ||
235 | // tei:0 pusi:1 tp:0 pid:0 0000 0010 0000 | ||
236 | 0x40, 0x10, | ||
237 | // tsc:01 afc:01 cc:0000 pointer_field:0000 0000 | ||
238 | 0x50, 0x00, | ||
239 | // tid:0000 0000 ssi:0 0:0 r:00 sl:0000 0010 1111 | ||
240 | 0x00, 0x00, 0x2f, | ||
241 | // pn:0000 0000 0000 0001 | ||
242 | 0x00, 0x01, | ||
243 | // r:00 vn:00 000 cni:1 sn:0000 0000 lsn:0000 0000 | ||
244 | 0x01, 0x00, 0x00, | ||
245 | // r:000 ppid:0 0011 1111 1111 | ||
246 | 0x03, 0xff, | ||
247 | // r:0000 pil:0000 0000 0000 | ||
248 | 0x00, 0x00, | ||
249 | // h264 | ||
250 | // st:0001 1010 r:000 epid:0 0000 0001 0001 | ||
251 | 0x1b, 0x00, 0x11, | ||
252 | // r:0000 esil:0000 0000 0000 | ||
253 | 0x00, 0x00, | ||
254 | // adts | ||
255 | // st:0000 1111 r:000 epid:0 0000 0001 0010 | ||
256 | 0x0f, 0x00, 0x12, | ||
257 | // r:0000 esil:0000 0000 0000 | ||
258 | 0x00, 0x00, | ||
259 | // crc | ||
260 | 0x00, 0x00, 0x00, 0x00 | ||
261 | ])); | ||
262 | |||
263 | ok(parseStream.programMapTable, 'parsed a program map'); | ||
264 | strictEqual(0x1b, parseStream.programMapTable[0x11], 'associated h264 with pid 0x11'); | ||
265 | strictEqual(0x0f, parseStream.programMapTable[0x12], 'associated adts with pid 0x12'); | ||
266 | }); | ||
267 | |||
27 | module('MP4 Transmuxer', { | 268 | module('MP4 Transmuxer', { |
28 | setup: function() { | 269 | setup: function() { |
29 | transmuxer = new Transmuxer(); | 270 | transmuxer = new Transmuxer(); | ... | ... |
-
Please register or sign in to post a comment