frame-utils.js
8.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/**
* mux.js
*
* Copyright (c) Brightcove
* Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
*/
// Convert an array of nal units into an array of frames with each frame being
// composed of the nal units that make up that frame
// Also keep track of cummulative data about the frame from the nal units such
// as the frame duration, starting pts, etc.
var groupNalsIntoFrames = function(nalUnits) {
var
i,
currentNal,
currentFrame = [],
frames = [];
// TODO added for LHLS, make sure this is OK
frames.byteLength = 0;
frames.nalCount = 0;
frames.duration = 0;
currentFrame.byteLength = 0;
for (i = 0; i < nalUnits.length; i++) {
currentNal = nalUnits[i];
// Split on 'aud'-type nal units
if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
// Since the very first nal unit is expected to be an AUD
// only push to the frames array when currentFrame is not empty
if (currentFrame.length) {
currentFrame.duration = currentNal.dts - currentFrame.dts;
// TODO added for LHLS, make sure this is OK
frames.byteLength += currentFrame.byteLength;
frames.nalCount += currentFrame.length;
frames.duration += currentFrame.duration;
frames.push(currentFrame);
}
currentFrame = [currentNal];
currentFrame.byteLength = currentNal.data.byteLength;
currentFrame.pts = currentNal.pts;
currentFrame.dts = currentNal.dts;
} else {
// Specifically flag key frames for ease of use later
if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
currentFrame.keyFrame = true;
}
currentFrame.duration = currentNal.dts - currentFrame.dts;
currentFrame.byteLength += currentNal.data.byteLength;
currentFrame.push(currentNal);
}
}
// For the last frame, use the duration of the previous frame if we
// have nothing better to go on
if (frames.length &&
(!currentFrame.duration ||
currentFrame.duration <= 0)) {
currentFrame.duration = frames[frames.length - 1].duration;
}
// Push the final frame
// TODO added for LHLS, make sure this is OK
frames.byteLength += currentFrame.byteLength;
frames.nalCount += currentFrame.length;
frames.duration += currentFrame.duration;
frames.push(currentFrame);
return frames;
};
// Convert an array of frames into an array of Gop with each Gop being composed
// of the frames that make up that Gop
// Also keep track of cummulative data about the Gop from the frames such as the
// Gop duration, starting pts, etc.
var groupFramesIntoGops = function(frames) {
var
i,
currentFrame,
currentGop = [],
gops = [];
// We must pre-set some of the values on the Gop since we
// keep running totals of these values
currentGop.byteLength = 0;
currentGop.nalCount = 0;
currentGop.duration = 0;
currentGop.pts = frames[0].pts;
currentGop.dts = frames[0].dts;
// store some metadata about all the Gops
gops.byteLength = 0;
gops.nalCount = 0;
gops.duration = 0;
gops.pts = frames[0].pts;
gops.dts = frames[0].dts;
for (i = 0; i < frames.length; i++) {
currentFrame = frames[i];
if (currentFrame.keyFrame) {
// Since the very first frame is expected to be an keyframe
// only push to the gops array when currentGop is not empty
if (currentGop.length) {
gops.push(currentGop);
gops.byteLength += currentGop.byteLength;
gops.nalCount += currentGop.nalCount;
gops.duration += currentGop.duration;
}
currentGop = [currentFrame];
currentGop.nalCount = currentFrame.length;
currentGop.byteLength = currentFrame.byteLength;
currentGop.pts = currentFrame.pts;
currentGop.dts = currentFrame.dts;
currentGop.duration = currentFrame.duration;
} else {
currentGop.duration += currentFrame.duration;
currentGop.nalCount += currentFrame.length;
currentGop.byteLength += currentFrame.byteLength;
currentGop.push(currentFrame);
}
}
if (gops.length && currentGop.duration <= 0) {
currentGop.duration = gops[gops.length - 1].duration;
}
gops.byteLength += currentGop.byteLength;
gops.nalCount += currentGop.nalCount;
gops.duration += currentGop.duration;
// push the final Gop
gops.push(currentGop);
return gops;
};
/*
* Search for the first keyframe in the GOPs and throw away all frames
* until that keyframe. Then extend the duration of the pulled keyframe
* and pull the PTS and DTS of the keyframe so that it covers the time
* range of the frames that were disposed.
*
* @param {Array} gops video GOPs
* @returns {Array} modified video GOPs
*/
var extendFirstKeyFrame = function(gops) {
var currentGop;
if (!gops[0][0].keyFrame && gops.length > 1) {
// Remove the first GOP
currentGop = gops.shift();
gops.byteLength -= currentGop.byteLength;
gops.nalCount -= currentGop.nalCount;
// Extend the first frame of what is now the
// first gop to cover the time period of the
// frames we just removed
gops[0][0].dts = currentGop.dts;
gops[0][0].pts = currentGop.pts;
gops[0][0].duration += currentGop.duration;
}
return gops;
};
/**
* Default sample object
* see ISO/IEC 14496-12:2012, section 8.6.4.3
*/
var createDefaultSample = function() {
return {
size: 0,
flags: {
isLeading: 0,
dependsOn: 1,
isDependedOn: 0,
hasRedundancy: 0,
degradationPriority: 0,
isNonSyncSample: 1
}
};
};
/*
* Collates information from a video frame into an object for eventual
* entry into an MP4 sample table.
*
* @param {Object} frame the video frame
* @param {Number} dataOffset the byte offset to position the sample
* @return {Object} object containing sample table info for a frame
*/
var sampleForFrame = function(frame, dataOffset) {
var sample = createDefaultSample();
sample.dataOffset = dataOffset;
sample.compositionTimeOffset = frame.pts - frame.dts;
sample.duration = frame.duration;
sample.size = 4 * frame.length; // Space for nal unit size
sample.size += frame.byteLength;
if (frame.keyFrame) {
sample.flags.dependsOn = 2;
sample.flags.isNonSyncSample = 0;
}
return sample;
};
// generate the track's sample table from an array of gops
var generateSampleTable = function(gops, baseDataOffset) {
var
h, i,
sample,
currentGop,
currentFrame,
dataOffset = baseDataOffset || 0,
samples = [];
for (h = 0; h < gops.length; h++) {
currentGop = gops[h];
for (i = 0; i < currentGop.length; i++) {
currentFrame = currentGop[i];
sample = sampleForFrame(currentFrame, dataOffset);
dataOffset += sample.size;
samples.push(sample);
}
}
return samples;
};
// generate the track's raw mdat data from an array of gops
var concatenateNalData = function(gops) {
var
h, i, j,
currentGop,
currentFrame,
currentNal,
dataOffset = 0,
nalsByteLength = gops.byteLength,
numberOfNals = gops.nalCount,
totalByteLength = nalsByteLength + 4 * numberOfNals,
data = new Uint8Array(totalByteLength),
view = new DataView(data.buffer);
// For each Gop..
for (h = 0; h < gops.length; h++) {
currentGop = gops[h];
// For each Frame..
for (i = 0; i < currentGop.length; i++) {
currentFrame = currentGop[i];
// For each NAL..
for (j = 0; j < currentFrame.length; j++) {
currentNal = currentFrame[j];
view.setUint32(dataOffset, currentNal.data.byteLength);
dataOffset += 4;
data.set(currentNal.data, dataOffset);
dataOffset += currentNal.data.byteLength;
}
}
}
return data;
};
// generate the track's sample table from a frame
var generateSampleTableForFrame = function(frame, baseDataOffset) {
var
sample,
dataOffset = baseDataOffset || 0,
samples = [];
sample = sampleForFrame(frame, dataOffset);
samples.push(sample);
return samples;
};
// generate the track's raw mdat data from a frame
var concatenateNalDataForFrame = function(frame) {
var
i,
currentNal,
dataOffset = 0,
nalsByteLength = frame.byteLength,
numberOfNals = frame.length,
totalByteLength = nalsByteLength + 4 * numberOfNals,
data = new Uint8Array(totalByteLength),
view = new DataView(data.buffer);
// For each NAL..
for (i = 0; i < frame.length; i++) {
currentNal = frame[i];
view.setUint32(dataOffset, currentNal.data.byteLength);
dataOffset += 4;
data.set(currentNal.data, dataOffset);
dataOffset += currentNal.data.byteLength;
}
return data;
};
module.exports = {
groupNalsIntoFrames: groupNalsIntoFrames,
groupFramesIntoGops: groupFramesIntoGops,
extendFirstKeyFrame: extendFirstKeyFrame,
generateSampleTable: generateSampleTable,
concatenateNalData: concatenateNalData,
generateSampleTableForFrame: generateSampleTableForFrame,
concatenateNalDataForFrame: concatenateNalDataForFrame
};