@editframe/assets 0.6.0-beta.9 → 0.7.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/av/MP4File.cjs +187 -0
- package/dist/lib/av/MP4File.js +170 -0
- package/dist/lib/util/execPromise.cjs +6 -0
- package/dist/lib/util/execPromise.js +6 -0
- package/dist/packages/assets/src/Probe.cjs +224 -0
- package/dist/packages/assets/src/Probe.d.ts +646 -0
- package/dist/packages/assets/src/Probe.js +207 -0
- package/dist/packages/assets/src/VideoRenderOptions.cjs +36 -0
- package/dist/packages/assets/src/VideoRenderOptions.d.ts +169 -0
- package/dist/packages/assets/src/VideoRenderOptions.js +36 -0
- package/dist/packages/assets/src/idempotentTask.cjs +57 -0
- package/dist/packages/assets/src/idempotentTask.d.ts +13 -0
- package/dist/packages/assets/src/idempotentTask.js +57 -0
- package/dist/packages/assets/src/index.cjs +20 -0
- package/dist/packages/assets/src/index.d.ts +9 -0
- package/dist/packages/assets/src/index.js +20 -0
- package/dist/packages/assets/src/md5.cjs +60 -0
- package/dist/packages/assets/src/md5.d.ts +6 -0
- package/dist/packages/assets/src/md5.js +60 -0
- package/dist/packages/assets/src/mp4FileWritable.cjs +21 -0
- package/dist/packages/assets/src/mp4FileWritable.d.ts +4 -0
- package/dist/packages/assets/src/mp4FileWritable.js +21 -0
- package/dist/packages/assets/src/tasks/cacheImage.cjs +22 -0
- package/dist/packages/assets/src/tasks/cacheImage.d.ts +1 -0
- package/dist/packages/assets/src/tasks/cacheImage.js +22 -0
- package/dist/packages/assets/src/tasks/findOrCreateCaptions.cjs +26 -0
- package/dist/packages/assets/src/tasks/findOrCreateCaptions.d.ts +1 -0
- package/dist/packages/assets/src/tasks/findOrCreateCaptions.js +26 -0
- package/dist/packages/assets/src/tasks/generateTrack.cjs +52 -0
- package/dist/packages/assets/src/tasks/generateTrack.d.ts +1 -0
- package/dist/packages/assets/src/tasks/generateTrack.js +52 -0
- package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.cjs +105 -0
- package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.d.ts +1 -0
- package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.js +105 -0
- package/package.json +10 -4
- package/src/tasks/cacheImage.ts +22 -0
- package/src/tasks/findOrCreateCaptions.ts +29 -0
- package/src/tasks/generateTrack.ts +61 -0
- package/src/tasks/generateTrackFragmentIndex.ts +120 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const MP4Box = require("mp4box");
|
|
4
|
+
const debug = require("debug");
|
|
5
|
+
function _interopNamespaceDefault(e) {
|
|
6
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
7
|
+
if (e) {
|
|
8
|
+
for (const k in e) {
|
|
9
|
+
if (k !== "default") {
|
|
10
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
11
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: () => e[k]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
n.default = e;
|
|
19
|
+
return Object.freeze(n);
|
|
20
|
+
}
|
|
21
|
+
const MP4Box__namespace = /* @__PURE__ */ _interopNamespaceDefault(MP4Box);
|
|
22
|
+
const log = debug("ef:av:mp4file");
|
|
23
|
+
class MP4File extends MP4Box__namespace.ISOFile {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
27
|
+
this.onReady = () => resolve();
|
|
28
|
+
this.onError = reject;
|
|
29
|
+
});
|
|
30
|
+
this.waitingForSamples = [];
|
|
31
|
+
this._hasSeenLastSamples = false;
|
|
32
|
+
this._arrayBufferFileStart = 0;
|
|
33
|
+
}
|
|
34
|
+
setSegmentOptions(id, user, options) {
|
|
35
|
+
const trak = this.getTrackById(id);
|
|
36
|
+
if (trak) {
|
|
37
|
+
trak.nextSample = 0;
|
|
38
|
+
this.fragmentedTracks.push({
|
|
39
|
+
id,
|
|
40
|
+
user,
|
|
41
|
+
trak,
|
|
42
|
+
segmentStream: null,
|
|
43
|
+
nb_samples: "nbSamples" in options && options.nbSamples || 1e3,
|
|
44
|
+
rapAlignement: ("rapAlignement" in options && options.rapAlignement) ?? true
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fragments all tracks in a file into separate array buffers.
|
|
50
|
+
*/
|
|
51
|
+
async fragmentAllTracks() {
|
|
52
|
+
const trackBuffers = {};
|
|
53
|
+
for await (const segment of this.fragmentIterator()) {
|
|
54
|
+
(trackBuffers[segment.track] ??= []).push(segment.data);
|
|
55
|
+
}
|
|
56
|
+
return trackBuffers;
|
|
57
|
+
}
|
|
58
|
+
async *fragmentIterator() {
|
|
59
|
+
await this.readyPromise;
|
|
60
|
+
const trackInfo = {};
|
|
61
|
+
for (const videoTrack of this.getInfo().videoTracks) {
|
|
62
|
+
trackInfo[videoTrack.id] = { index: 0, complete: false };
|
|
63
|
+
this.setSegmentOptions(videoTrack.id, null, {
|
|
64
|
+
rapAlignement: true
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const audioTrack of this.getInfo().audioTracks) {
|
|
68
|
+
trackInfo[audioTrack.id] = { index: 0, complete: false };
|
|
69
|
+
const sampleRate = audioTrack.audio.sample_rate;
|
|
70
|
+
const probablePacketSize = 1024;
|
|
71
|
+
const probableFourSecondsOfSamples = Math.ceil(
|
|
72
|
+
sampleRate / probablePacketSize * 4
|
|
73
|
+
);
|
|
74
|
+
this.setSegmentOptions(audioTrack.id, null, {
|
|
75
|
+
nbSamples: probableFourSecondsOfSamples
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const initSegments = this.initializeSegmentation();
|
|
79
|
+
for (const initSegment of initSegments) {
|
|
80
|
+
yield {
|
|
81
|
+
track: initSegment.id,
|
|
82
|
+
segment: "init",
|
|
83
|
+
data: initSegment.buffer,
|
|
84
|
+
complete: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const fragmentStartSamples = {};
|
|
88
|
+
let finishedReading = false;
|
|
89
|
+
const allTracksFinished = () => {
|
|
90
|
+
for (const fragmentedTrack of this.fragmentedTracks) {
|
|
91
|
+
if (!trackInfo[fragmentedTrack.id]?.complete) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
while (!(finishedReading && allTracksFinished())) {
|
|
98
|
+
for (const fragTrak of this.fragmentedTracks) {
|
|
99
|
+
const trak = fragTrak.trak;
|
|
100
|
+
if (trak.nextSample === void 0) {
|
|
101
|
+
throw new Error("trak.nextSample is undefined");
|
|
102
|
+
}
|
|
103
|
+
if (trak.samples === void 0) {
|
|
104
|
+
throw new Error("trak.samples is undefined");
|
|
105
|
+
}
|
|
106
|
+
while (trak.nextSample < trak.samples.length) {
|
|
107
|
+
let result = void 0;
|
|
108
|
+
const fragTrakNextSample = trak.samples[trak.nextSample];
|
|
109
|
+
if (fragTrakNextSample) {
|
|
110
|
+
fragmentStartSamples[fragTrak.id] ||= fragTrakNextSample;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
result = this.createFragment(
|
|
114
|
+
fragTrak.id,
|
|
115
|
+
trak.nextSample,
|
|
116
|
+
fragTrak.segmentStream
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("Failed to createFragment", error);
|
|
120
|
+
}
|
|
121
|
+
if (result) {
|
|
122
|
+
fragTrak.segmentStream = result;
|
|
123
|
+
trak.nextSample++;
|
|
124
|
+
} else {
|
|
125
|
+
finishedReading = await this.waitForMoreSamples();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const nextSample = trak.samples[trak.nextSample];
|
|
129
|
+
const emitSegment = (
|
|
130
|
+
// if rapAlignement is true, we emit a fragment when we have a rap sample coming up next
|
|
131
|
+
fragTrak.rapAlignement === true && nextSample?.is_sync || // if rapAlignement is false, we emit a fragment when we have the required number of samples
|
|
132
|
+
!fragTrak.rapAlignement && trak.nextSample % fragTrak.nb_samples === 0 || // // if this is the last sample, we emit the fragment
|
|
133
|
+
// finished ||
|
|
134
|
+
// if we have more samples than the number of samples requested, we emit the fragment
|
|
135
|
+
trak.nextSample >= trak.samples.length
|
|
136
|
+
);
|
|
137
|
+
if (emitSegment) {
|
|
138
|
+
const trackInfoForFrag = trackInfo[fragTrak.id];
|
|
139
|
+
if (!trackInfoForFrag) {
|
|
140
|
+
throw new Error("trackInfoForFrag is undefined");
|
|
141
|
+
}
|
|
142
|
+
if (trak.nextSample >= trak.samples.length) {
|
|
143
|
+
trackInfoForFrag.complete = true;
|
|
144
|
+
}
|
|
145
|
+
log(
|
|
146
|
+
`Yielding fragment #${trackInfoForFrag.index} for track=${fragTrak.id}`
|
|
147
|
+
);
|
|
148
|
+
const startSample = fragmentStartSamples[fragTrak.id];
|
|
149
|
+
const endSample = trak.samples[trak.nextSample - 1];
|
|
150
|
+
if (!startSample || !endSample) {
|
|
151
|
+
throw new Error("startSample or endSample is undefined");
|
|
152
|
+
}
|
|
153
|
+
yield {
|
|
154
|
+
track: fragTrak.id,
|
|
155
|
+
segment: trackInfoForFrag.index,
|
|
156
|
+
data: fragTrak.segmentStream.buffer,
|
|
157
|
+
complete: trackInfoForFrag.complete,
|
|
158
|
+
cts: startSample.cts,
|
|
159
|
+
dts: startSample.dts,
|
|
160
|
+
duration: endSample.cts - startSample.cts + endSample.duration
|
|
161
|
+
};
|
|
162
|
+
trackInfoForFrag.index += 1;
|
|
163
|
+
fragTrak.segmentStream = null;
|
|
164
|
+
delete fragmentStartSamples[fragTrak.id];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finishedReading = await this.waitForMoreSamples();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
waitForMoreSamples() {
|
|
172
|
+
if (this._hasSeenLastSamples) {
|
|
173
|
+
return Promise.resolve(true);
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
this.waitingForSamples.push(resolve);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
processSamples(last) {
|
|
180
|
+
this._hasSeenLastSamples = last;
|
|
181
|
+
for (const observer of this.waitingForSamples) {
|
|
182
|
+
observer(last);
|
|
183
|
+
}
|
|
184
|
+
this.waitingForSamples = [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.MP4File = MP4File;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as MP4Box from "mp4box";
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
const log = debug("ef:av:mp4file");
|
|
4
|
+
class MP4File extends MP4Box.ISOFile {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
8
|
+
this.onReady = () => resolve();
|
|
9
|
+
this.onError = reject;
|
|
10
|
+
});
|
|
11
|
+
this.waitingForSamples = [];
|
|
12
|
+
this._hasSeenLastSamples = false;
|
|
13
|
+
this._arrayBufferFileStart = 0;
|
|
14
|
+
}
|
|
15
|
+
setSegmentOptions(id, user, options) {
|
|
16
|
+
const trak = this.getTrackById(id);
|
|
17
|
+
if (trak) {
|
|
18
|
+
trak.nextSample = 0;
|
|
19
|
+
this.fragmentedTracks.push({
|
|
20
|
+
id,
|
|
21
|
+
user,
|
|
22
|
+
trak,
|
|
23
|
+
segmentStream: null,
|
|
24
|
+
nb_samples: "nbSamples" in options && options.nbSamples || 1e3,
|
|
25
|
+
rapAlignement: ("rapAlignement" in options && options.rapAlignement) ?? true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fragments all tracks in a file into separate array buffers.
|
|
31
|
+
*/
|
|
32
|
+
async fragmentAllTracks() {
|
|
33
|
+
const trackBuffers = {};
|
|
34
|
+
for await (const segment of this.fragmentIterator()) {
|
|
35
|
+
(trackBuffers[segment.track] ??= []).push(segment.data);
|
|
36
|
+
}
|
|
37
|
+
return trackBuffers;
|
|
38
|
+
}
|
|
39
|
+
async *fragmentIterator() {
|
|
40
|
+
await this.readyPromise;
|
|
41
|
+
const trackInfo = {};
|
|
42
|
+
for (const videoTrack of this.getInfo().videoTracks) {
|
|
43
|
+
trackInfo[videoTrack.id] = { index: 0, complete: false };
|
|
44
|
+
this.setSegmentOptions(videoTrack.id, null, {
|
|
45
|
+
rapAlignement: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const audioTrack of this.getInfo().audioTracks) {
|
|
49
|
+
trackInfo[audioTrack.id] = { index: 0, complete: false };
|
|
50
|
+
const sampleRate = audioTrack.audio.sample_rate;
|
|
51
|
+
const probablePacketSize = 1024;
|
|
52
|
+
const probableFourSecondsOfSamples = Math.ceil(
|
|
53
|
+
sampleRate / probablePacketSize * 4
|
|
54
|
+
);
|
|
55
|
+
this.setSegmentOptions(audioTrack.id, null, {
|
|
56
|
+
nbSamples: probableFourSecondsOfSamples
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const initSegments = this.initializeSegmentation();
|
|
60
|
+
for (const initSegment of initSegments) {
|
|
61
|
+
yield {
|
|
62
|
+
track: initSegment.id,
|
|
63
|
+
segment: "init",
|
|
64
|
+
data: initSegment.buffer,
|
|
65
|
+
complete: false
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const fragmentStartSamples = {};
|
|
69
|
+
let finishedReading = false;
|
|
70
|
+
const allTracksFinished = () => {
|
|
71
|
+
for (const fragmentedTrack of this.fragmentedTracks) {
|
|
72
|
+
if (!trackInfo[fragmentedTrack.id]?.complete) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
while (!(finishedReading && allTracksFinished())) {
|
|
79
|
+
for (const fragTrak of this.fragmentedTracks) {
|
|
80
|
+
const trak = fragTrak.trak;
|
|
81
|
+
if (trak.nextSample === void 0) {
|
|
82
|
+
throw new Error("trak.nextSample is undefined");
|
|
83
|
+
}
|
|
84
|
+
if (trak.samples === void 0) {
|
|
85
|
+
throw new Error("trak.samples is undefined");
|
|
86
|
+
}
|
|
87
|
+
while (trak.nextSample < trak.samples.length) {
|
|
88
|
+
let result = void 0;
|
|
89
|
+
const fragTrakNextSample = trak.samples[trak.nextSample];
|
|
90
|
+
if (fragTrakNextSample) {
|
|
91
|
+
fragmentStartSamples[fragTrak.id] ||= fragTrakNextSample;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
result = this.createFragment(
|
|
95
|
+
fragTrak.id,
|
|
96
|
+
trak.nextSample,
|
|
97
|
+
fragTrak.segmentStream
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error("Failed to createFragment", error);
|
|
101
|
+
}
|
|
102
|
+
if (result) {
|
|
103
|
+
fragTrak.segmentStream = result;
|
|
104
|
+
trak.nextSample++;
|
|
105
|
+
} else {
|
|
106
|
+
finishedReading = await this.waitForMoreSamples();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const nextSample = trak.samples[trak.nextSample];
|
|
110
|
+
const emitSegment = (
|
|
111
|
+
// if rapAlignement is true, we emit a fragment when we have a rap sample coming up next
|
|
112
|
+
fragTrak.rapAlignement === true && nextSample?.is_sync || // if rapAlignement is false, we emit a fragment when we have the required number of samples
|
|
113
|
+
!fragTrak.rapAlignement && trak.nextSample % fragTrak.nb_samples === 0 || // // if this is the last sample, we emit the fragment
|
|
114
|
+
// finished ||
|
|
115
|
+
// if we have more samples than the number of samples requested, we emit the fragment
|
|
116
|
+
trak.nextSample >= trak.samples.length
|
|
117
|
+
);
|
|
118
|
+
if (emitSegment) {
|
|
119
|
+
const trackInfoForFrag = trackInfo[fragTrak.id];
|
|
120
|
+
if (!trackInfoForFrag) {
|
|
121
|
+
throw new Error("trackInfoForFrag is undefined");
|
|
122
|
+
}
|
|
123
|
+
if (trak.nextSample >= trak.samples.length) {
|
|
124
|
+
trackInfoForFrag.complete = true;
|
|
125
|
+
}
|
|
126
|
+
log(
|
|
127
|
+
`Yielding fragment #${trackInfoForFrag.index} for track=${fragTrak.id}`
|
|
128
|
+
);
|
|
129
|
+
const startSample = fragmentStartSamples[fragTrak.id];
|
|
130
|
+
const endSample = trak.samples[trak.nextSample - 1];
|
|
131
|
+
if (!startSample || !endSample) {
|
|
132
|
+
throw new Error("startSample or endSample is undefined");
|
|
133
|
+
}
|
|
134
|
+
yield {
|
|
135
|
+
track: fragTrak.id,
|
|
136
|
+
segment: trackInfoForFrag.index,
|
|
137
|
+
data: fragTrak.segmentStream.buffer,
|
|
138
|
+
complete: trackInfoForFrag.complete,
|
|
139
|
+
cts: startSample.cts,
|
|
140
|
+
dts: startSample.dts,
|
|
141
|
+
duration: endSample.cts - startSample.cts + endSample.duration
|
|
142
|
+
};
|
|
143
|
+
trackInfoForFrag.index += 1;
|
|
144
|
+
fragTrak.segmentStream = null;
|
|
145
|
+
delete fragmentStartSamples[fragTrak.id];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finishedReading = await this.waitForMoreSamples();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
waitForMoreSamples() {
|
|
153
|
+
if (this._hasSeenLastSamples) {
|
|
154
|
+
return Promise.resolve(true);
|
|
155
|
+
}
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
this.waitingForSamples.push(resolve);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
processSamples(last) {
|
|
161
|
+
this._hasSeenLastSamples = last;
|
|
162
|
+
for (const observer of this.waitingForSamples) {
|
|
163
|
+
observer(last);
|
|
164
|
+
}
|
|
165
|
+
this.waitingForSamples = [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
MP4File
|
|
170
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const node_child_process = require("node:child_process");
|
|
4
|
+
const node_util = require("node:util");
|
|
5
|
+
const execPromise = node_util.promisify(node_child_process.exec);
|
|
6
|
+
exports.execPromise = execPromise;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const node_child_process = require("node:child_process");
|
|
4
|
+
const node_fs = require("node:fs");
|
|
5
|
+
const z = require("zod");
|
|
6
|
+
const debug = require("debug");
|
|
7
|
+
const execPromise = require("../../../lib/util/execPromise.cjs");
|
|
8
|
+
function _interopNamespaceDefault(e) {
|
|
9
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
10
|
+
if (e) {
|
|
11
|
+
for (const k in e) {
|
|
12
|
+
if (k !== "default") {
|
|
13
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: () => e[k]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
const z__namespace = /* @__PURE__ */ _interopNamespaceDefault(z);
|
|
25
|
+
const log = debug("ef:assets:probe");
|
|
26
|
+
const AudioStreamSchema = z__namespace.object({
|
|
27
|
+
index: z__namespace.number(),
|
|
28
|
+
codec_name: z__namespace.string(),
|
|
29
|
+
codec_long_name: z__namespace.string(),
|
|
30
|
+
codec_type: z__namespace.literal("audio"),
|
|
31
|
+
codec_tag_string: z__namespace.string(),
|
|
32
|
+
codec_tag: z__namespace.string(),
|
|
33
|
+
sample_fmt: z__namespace.string(),
|
|
34
|
+
sample_rate: z__namespace.string(),
|
|
35
|
+
channels: z__namespace.number(),
|
|
36
|
+
channel_layout: z__namespace.string(),
|
|
37
|
+
bits_per_sample: z__namespace.number(),
|
|
38
|
+
initial_padding: z__namespace.number().optional(),
|
|
39
|
+
r_frame_rate: z__namespace.string(),
|
|
40
|
+
avg_frame_rate: z__namespace.string(),
|
|
41
|
+
time_base: z__namespace.string(),
|
|
42
|
+
start_pts: z__namespace.number(),
|
|
43
|
+
start_time: z__namespace.coerce.number(),
|
|
44
|
+
duration_ts: z__namespace.number(),
|
|
45
|
+
duration: z__namespace.coerce.number(),
|
|
46
|
+
bit_rate: z__namespace.string(),
|
|
47
|
+
disposition: z__namespace.record(z__namespace.unknown())
|
|
48
|
+
});
|
|
49
|
+
const VideoStreamSchema = z__namespace.object({
|
|
50
|
+
index: z__namespace.number(),
|
|
51
|
+
codec_name: z__namespace.string(),
|
|
52
|
+
codec_long_name: z__namespace.string(),
|
|
53
|
+
codec_type: z__namespace.literal("video"),
|
|
54
|
+
codec_tag_string: z__namespace.string(),
|
|
55
|
+
codec_tag: z__namespace.string(),
|
|
56
|
+
width: z__namespace.number(),
|
|
57
|
+
height: z__namespace.number(),
|
|
58
|
+
coded_width: z__namespace.number(),
|
|
59
|
+
coded_height: z__namespace.number(),
|
|
60
|
+
r_frame_rate: z__namespace.string(),
|
|
61
|
+
avg_frame_rate: z__namespace.string(),
|
|
62
|
+
time_base: z__namespace.string(),
|
|
63
|
+
start_pts: z__namespace.number().optional(),
|
|
64
|
+
start_time: z__namespace.coerce.number().optional(),
|
|
65
|
+
duration_ts: z__namespace.number().optional(),
|
|
66
|
+
duration: z__namespace.coerce.number().optional(),
|
|
67
|
+
bit_rate: z__namespace.string().optional(),
|
|
68
|
+
disposition: z__namespace.record(z__namespace.unknown())
|
|
69
|
+
});
|
|
70
|
+
const ProbeFormatSchema = z__namespace.object({
|
|
71
|
+
filename: z__namespace.string(),
|
|
72
|
+
nb_streams: z__namespace.number(),
|
|
73
|
+
nb_programs: z__namespace.number(),
|
|
74
|
+
format_name: z__namespace.string(),
|
|
75
|
+
format_long_name: z__namespace.string(),
|
|
76
|
+
start_time: z__namespace.string().optional(),
|
|
77
|
+
duration: z__namespace.string().optional(),
|
|
78
|
+
size: z__namespace.string(),
|
|
79
|
+
bit_rate: z__namespace.string().optional(),
|
|
80
|
+
probe_score: z__namespace.number()
|
|
81
|
+
});
|
|
82
|
+
const StreamSchema = z__namespace.discriminatedUnion("codec_type", [
|
|
83
|
+
AudioStreamSchema,
|
|
84
|
+
VideoStreamSchema
|
|
85
|
+
]);
|
|
86
|
+
const ProbeSchema = z__namespace.object({
|
|
87
|
+
streams: z__namespace.array(StreamSchema),
|
|
88
|
+
format: ProbeFormatSchema
|
|
89
|
+
});
|
|
90
|
+
class Probe {
|
|
91
|
+
constructor(absolutePath, rawData) {
|
|
92
|
+
this.absolutePath = absolutePath;
|
|
93
|
+
this.data = ProbeSchema.parse(rawData);
|
|
94
|
+
}
|
|
95
|
+
static async probePath(absolutePath) {
|
|
96
|
+
const probeCommand = `ffprobe -v error -show_format -show_streams -of json ${absolutePath}`;
|
|
97
|
+
log("Probing", probeCommand);
|
|
98
|
+
const probeResult = await execPromise.execPromise(probeCommand);
|
|
99
|
+
log("Probe result", probeResult.stdout);
|
|
100
|
+
log("Probe stderr", probeResult.stderr);
|
|
101
|
+
const json = JSON.parse(probeResult.stdout);
|
|
102
|
+
return new Probe(absolutePath, json);
|
|
103
|
+
}
|
|
104
|
+
get audioStreams() {
|
|
105
|
+
return this.data.streams.filter(
|
|
106
|
+
(stream) => stream.codec_type === "audio"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
get videoStreams() {
|
|
110
|
+
return this.data.streams.filter(
|
|
111
|
+
(stream) => stream.codec_type === "video"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
get streams() {
|
|
115
|
+
return this.data.streams;
|
|
116
|
+
}
|
|
117
|
+
get format() {
|
|
118
|
+
return this.data.format;
|
|
119
|
+
}
|
|
120
|
+
get mustReencodeAudio() {
|
|
121
|
+
return this.audioStreams.some((stream) => stream.codec_name !== "aac");
|
|
122
|
+
}
|
|
123
|
+
get mustReencodeVideo() {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
get mustRemux() {
|
|
127
|
+
return this.format.format_name !== "mp4";
|
|
128
|
+
}
|
|
129
|
+
get hasAudio() {
|
|
130
|
+
return this.audioStreams.length > 0;
|
|
131
|
+
}
|
|
132
|
+
get hasVideo() {
|
|
133
|
+
return this.videoStreams.length > 0;
|
|
134
|
+
}
|
|
135
|
+
get isAudioOnly() {
|
|
136
|
+
return this.audioStreams.length > 0 && this.videoStreams.length === 0;
|
|
137
|
+
}
|
|
138
|
+
get isVideoOnly() {
|
|
139
|
+
return this.audioStreams.length === 0 && this.videoStreams.length > 0;
|
|
140
|
+
}
|
|
141
|
+
get mustProcess() {
|
|
142
|
+
return this.mustReencodeAudio || this.mustReencodeVideo || this.mustRemux;
|
|
143
|
+
}
|
|
144
|
+
get ffmpegAudioOptions() {
|
|
145
|
+
if (!this.hasAudio) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
if (this.mustReencodeAudio) {
|
|
149
|
+
return [
|
|
150
|
+
"-c:a",
|
|
151
|
+
"aac",
|
|
152
|
+
"-b:a",
|
|
153
|
+
"192k"
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
return ["-c:a", "copy"];
|
|
157
|
+
}
|
|
158
|
+
get ffmpegVideoOptions() {
|
|
159
|
+
if (!this.hasVideo) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
if (this.mustReencodeVideo) {
|
|
163
|
+
return [
|
|
164
|
+
"-c:v",
|
|
165
|
+
"h264",
|
|
166
|
+
// Filter out SEI NAL units that aren't supported by the webcodecs decoder
|
|
167
|
+
"-bsf:v",
|
|
168
|
+
"filter_units=remove_types=6",
|
|
169
|
+
"-pix_fmt",
|
|
170
|
+
"yuv420p"
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
return [
|
|
174
|
+
"-c:v",
|
|
175
|
+
"copy",
|
|
176
|
+
// Filter out SEI NAL units that aren't supported by the webcodecs decoder
|
|
177
|
+
"-bsf:v",
|
|
178
|
+
"filter_units=remove_types=6"
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
createConformingReadstream() {
|
|
182
|
+
if (!this.mustProcess) {
|
|
183
|
+
return node_fs.createReadStream(this.absolutePath);
|
|
184
|
+
}
|
|
185
|
+
const ffmpegConformanceArgs = [
|
|
186
|
+
"-i",
|
|
187
|
+
this.absolutePath,
|
|
188
|
+
...this.ffmpegAudioOptions,
|
|
189
|
+
...this.ffmpegVideoOptions,
|
|
190
|
+
"-f",
|
|
191
|
+
"mp4",
|
|
192
|
+
"-movflags",
|
|
193
|
+
"cmaf+frag_keyframe+empty_moov",
|
|
194
|
+
"pipe:1"
|
|
195
|
+
];
|
|
196
|
+
log("Running ffmpeg", ffmpegConformanceArgs);
|
|
197
|
+
const ffmpegConformer = node_child_process.spawn("ffmpeg", ffmpegConformanceArgs, {
|
|
198
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
199
|
+
});
|
|
200
|
+
ffmpegConformer.stderr.on("data", (data) => {
|
|
201
|
+
log(data.toString());
|
|
202
|
+
});
|
|
203
|
+
const ffmpegFragmentArgs = [
|
|
204
|
+
"-i",
|
|
205
|
+
"-",
|
|
206
|
+
"-c",
|
|
207
|
+
"copy",
|
|
208
|
+
"-f",
|
|
209
|
+
"mp4",
|
|
210
|
+
"-movflags",
|
|
211
|
+
"frag_keyframe+empty_moov",
|
|
212
|
+
"pipe:1"
|
|
213
|
+
];
|
|
214
|
+
log("Running ffmpeg", ffmpegFragmentArgs);
|
|
215
|
+
const ffmpegFragmenter = node_child_process.spawn("ffmpeg", ffmpegFragmentArgs, {
|
|
216
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
217
|
+
});
|
|
218
|
+
ffmpegConformer.stdout.pipe(ffmpegFragmenter.stdin);
|
|
219
|
+
return ffmpegFragmenter.stdout;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.AudioStreamSchema = AudioStreamSchema;
|
|
223
|
+
exports.Probe = Probe;
|
|
224
|
+
exports.VideoStreamSchema = VideoStreamSchema;
|