-
Notifications
You must be signed in to change notification settings - Fork 3
/
mp4.js
138 lines (113 loc) · 4.56 KB
/
mp4.js
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
var mp4box = require('mp4box')
var needle = require('needle')
var fs = require('fs')
function iterateCounts(counts, values, fn) {
var idx = 0;
counts.forEach(function(count, i) {
for (var j = 0; j!=count; j++) { fn(values[i], idx++) }
})
}
function getFramesFromBox(box)
{
//box.buildSampleLists();
//var samples = box.moov.traks[0].samples;
var track = box.moov.traks.filter(function(t) {
return t.mdia.minf.stbl.stss
})[0];
//var stsz = track.mdia.minf.stbl.stsz; // sample table sizes - that's in bytes
var stts = track.mdia.minf.stbl.stts; // sample table time to sample map
var ctts = track.mdia.minf.stbl.ctts; // Composition Time Offset - used to convert DTS to PTS
var mdhd = track.mdia.mdhd; // media header
// from stts documentation at https://wiki.multimedia.cx/?title=QuickTime_container#stss
// duration = (sample_count1 * sample_time_delta1 + ... + sample_countN * sample_time_deltaN ) / timescale
var allDts = [ ];
iterateCounts(stts.sample_counts, stts.sample_deltas, function(delta, idx) { allDts.push(idx * delta) });
// use ctts to build pts - https://wiki.multimedia.cx/?title=QuickTime_container#ctts
//console.log(track.mdia.minf.stbl.ctts.sample_counts.length, track.mdia.minf.stbl.ctts.sample_offsets.length, mdhd)
var allPts = [];
if (ctts) iterateCounts(ctts.sample_counts, ctts.sample_offsets, function(offset, idx) { allPts.push(allDts[idx] + offset) });
// we need the stss box - moov.traks[<trackNum>].mdia.minf.stbl.stss - http://wiki.multimedia.cx/?title=QuickTime_container#stss
// stss - "Sync samples are also known as keyframes or intra-coded frames."
var frames = track.mdia.minf.stbl.stss.sample_numbers.map(function(x) {
// WARNING: in the BBB video, to match ffmpeg we need x+1, in the other, we need x-1; wtf?
// samples[x].dts/mdhd.timescale
var dts = allDts[x-1] / mdhd.timescale * 1000;
var pts = ctts ? allPts[x-1] / mdhd.timescale * 1000 : dts;
return { dts: dts, pts: pts, timestamp: pts, index: x }
});
// http://bit.ly/1MKue5R - there's a keyframe at the beginning
if (frames[0] && frames[0].index !== 1) frames.unshift({ timestamp: 0, dts: 0, index: 1 })
return frames
}
function getForMp4(url, cb) {
var box = mp4box.createFile();
var err, res, pos = 0;
var maxSeeks = 0;
function toArrayBuffer(buffer) {
var ab = new ArrayBuffer(buffer.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buffer.length; ++i) view[i] = buffer[i];
return ab;
}
function onData(buf) {
var b = toArrayBuffer(buf);
b.fileStart = pos;
pos += b.byteLength;
box.appendBuffer(b);
if (box.mdats.length && !box.moovStartFound) {
var offset = box.boxes.map(function(x) { return x.size }).reduce(function(a,b) { return a+b }, 0);
if (offset > lastOffset) {
if (maxSeeks > 3) {
stream.close ? stream.close() : stream.end();
return cb(new Error('maxSeeks exceeded'));
}
maxSeeks++;
startStream(url, offset);
}
}
}
var stream, lastOffset = 0;
function closeStream() {
if (!stream) return;
// credit to mafintosh/pump
if (stream instanceof fs.ReadStream && typeof(stream.close) === 'function') return stream.close() // use close for fs streams to avoid fd leaks
if (stream.request && typeof(stream.request.abort) === 'function') return stream.request.abort() // request.destroy just do .end - .abort is what we want
if (typeof(stream.destroy) === 'function') return stream.destroy()
}
function startStream(url, offset) {
//console.log("open stream at "+offset);
closeStream()
lastOffset = offset;
pos = offset;
if (/^http(s?):\/\//.test(url)) {
// TODO: WARNING: we should check if the source supports range headers
// and if the returned range corresponds to the requested range
// Otherwise, we will eventually end up with a maxSeeks exception (as we would read the beginning of the file over and over, thinking it's actually the next part)
stream = needle.get(url, { headers: { range: "bytes="+offset+"-" } })
.on('error', box.onError)
.on('end', function(e) { box.flush() })
.on('data', onData)
} else {
stream = fs.createReadStream(url, { start: offset })
.on('error', box.onError)
.on('end', function() { box.flush() })
.on('data', onData)
}
}
box.onError = function(err) {
closeStream()
cb(err);
};
startStream(url, 0);
box.onReady = function(info) {
box.flush()
closeStream()
if (!info) return cb(new Error("no info returned"));
if (!info.videoTracks[0]) return cb(new Error("no videoTracks[0]"))
try {
var frames = getFramesFromBox(box)
cb(null, frames)
} catch(e) { cb(e) }
}
}
module.exports = getForMp4