straight_to_video 0.0.11 → 0.0.12
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/app/assets/javascripts/straight-to-video.js +46 -22
- data/index.js +45 -21
- data/lib/straight_to_video/version.rb +1 -1
- data/package-lock.json +2 -2
- data/package.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c562c9b7243f141e7524a58e115bd257d5207bd7272bf01547812e9845235e75
|
|
4
|
+
data.tar.gz: bbe4b5f3e82e6d84fb928c9165d5c8935416ed6c04034c55db44c740db949785
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 368159b6977e3d37b116a55a35cca1d849029657ae86b01aa8113f9385288dab86f140d98a5bbb4d9639464e7e2ef8c362e204b52c45e5e2c36927e9aaa4db93
|
|
7
|
+
data.tar.gz: 8acd9f7af6b1293e7b2a6bd4626e6dc0897a09811684bcf8a7e27260dd48071306c1aba8b61c6b8d9a76f1aac0f5552eb8edd43f0e7e9cc9259e800531a3803d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.12
|
|
4
|
+
|
|
5
|
+
* Bound the WebCodecs encoder queue so long videos cannot exhaust WebKit memory.
|
|
6
|
+
* Keep browser encoding at or below the source video's bitrate.
|
|
7
|
+
|
|
3
8
|
## 0.0.11
|
|
4
9
|
|
|
5
10
|
* Work around WebKit labeling the requested first HEVC keyframe as a delta packet.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// straight-to-video@0.0.
|
|
1
|
+
// straight-to-video@0.0.12 vendored by the straight_to_video gem
|
|
2
2
|
// straight-to-video - https://github.com/searlsco/straight-to-video
|
|
3
3
|
|
|
4
4
|
// ----- External imports -----
|
|
@@ -14,6 +14,7 @@ const TARGET_VIDEO_BITRATE = 12_000_000
|
|
|
14
14
|
const TARGET_AUDIO_BITRATE = 96_000
|
|
15
15
|
const TARGET_AUDIO_SR = 48_000
|
|
16
16
|
const TARGET_AUDIO_CHANNELS = 2
|
|
17
|
+
const MAX_VIDEO_ENCODER_QUEUE_SIZE = 4
|
|
17
18
|
|
|
18
19
|
// ----- Video metadata probe -----
|
|
19
20
|
async function probeVideo (file) {
|
|
@@ -34,34 +35,47 @@ async function probeVideo (file) {
|
|
|
34
35
|
})
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
async function
|
|
38
|
+
async function estimateSourceVideoStats (file) {
|
|
38
39
|
try {
|
|
39
40
|
const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS })
|
|
40
41
|
const tracks = await input.getTracks()
|
|
41
42
|
const video = tracks.find(t => typeof t.isVideoTrack === 'function' && t.isVideoTrack())
|
|
42
|
-
if (!video) return 0
|
|
43
|
+
if (!video) return { fps: 0, bitrate: 0 }
|
|
43
44
|
const sink = new EncodedPacketSink(video)
|
|
44
45
|
const durations = []
|
|
46
|
+
let firstTimestamp = Infinity
|
|
47
|
+
let lastTimestamp = -Infinity
|
|
48
|
+
let totalBytes = 0
|
|
45
49
|
for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
|
|
46
|
-
const
|
|
47
|
-
if (packet.timestamp >= 0 && Number.isFinite(
|
|
50
|
+
const duration = Number(packet?.duration)
|
|
51
|
+
if (packet.timestamp >= 0 && Number.isFinite(duration) && duration > 0) {
|
|
52
|
+
durations.push(duration)
|
|
53
|
+
firstTimestamp = Math.min(firstTimestamp, packet.timestamp)
|
|
54
|
+
lastTimestamp = Math.max(lastTimestamp, packet.timestamp + duration)
|
|
55
|
+
totalBytes += packet.byteLength
|
|
56
|
+
}
|
|
48
57
|
if (durations.length >= 120) break
|
|
49
58
|
}
|
|
50
|
-
if (!durations.length) return 0
|
|
59
|
+
if (!durations.length) return { fps: 0, bitrate: 0 }
|
|
51
60
|
durations.sort((a, b) => a - b)
|
|
52
|
-
const
|
|
53
|
-
|
|
61
|
+
const duration = durations[Math.floor(durations.length / 2)]
|
|
62
|
+
const sampledDuration = lastTimestamp - firstTimestamp
|
|
63
|
+
return {
|
|
64
|
+
fps: Number.isFinite(duration) && duration > 0 ? (1 / duration) : 0,
|
|
65
|
+
bitrate: sampledDuration > 0 ? (totalBytes * 8 / sampledDuration) : 0
|
|
66
|
+
}
|
|
54
67
|
} catch (_) {
|
|
55
|
-
return 0
|
|
68
|
+
return { fps: 0, bitrate: 0 }
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
async function
|
|
72
|
+
async function determineEncodingPlan (file, { width, height }) {
|
|
60
73
|
const maxFps = Math.max(width, height) <= 1920 ? 30 : 60
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
const source = await estimateSourceVideoStats(file)
|
|
75
|
+
return {
|
|
76
|
+
fps: maxFps === 30 ? 30 : (source.fps >= 45 ? 60 : 30),
|
|
77
|
+
bitrate: source.bitrate > 0 ? Math.min(TARGET_VIDEO_BITRATE, Math.round(source.bitrate)) : TARGET_VIDEO_BITRATE
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
// ----- Audio helpers -----
|
|
@@ -125,8 +139,8 @@ async function canOptimizeVideo (file) {
|
|
|
125
139
|
const scale = Math.min(1, MAX_LONG_SIDE / Math.max(2, long))
|
|
126
140
|
const targetWidth = Math.max(2, Math.round(width * scale))
|
|
127
141
|
const targetHeight = Math.max(2, Math.round(height * scale))
|
|
128
|
-
const
|
|
129
|
-
const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight,
|
|
142
|
+
const plan = await determineEncodingPlan(file, { width, height })
|
|
143
|
+
const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, ...plan }).then(() => true).catch(() => false)
|
|
130
144
|
if (!sup) return { ok: false, reason: 'unsupported-video-config', message: 'No supported encoder configuration for this resolution on this device.' }
|
|
131
145
|
|
|
132
146
|
// Header sniffing when file.type is empty/incorrect
|
|
@@ -142,7 +156,7 @@ async function canOptimizeVideo (file) {
|
|
|
142
156
|
const hasEbml = buf.length >= 4 && buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3
|
|
143
157
|
if (!(hasFtyp || hasEbml)) return { ok: false, reason: 'unknown-container', message: 'Unrecognized container; expected MP4/MOV or WebM.' }
|
|
144
158
|
}
|
|
145
|
-
return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight,
|
|
159
|
+
return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, ...plan } }
|
|
146
160
|
} catch (e) {
|
|
147
161
|
return { ok: false, reason: 'probe-failed', message: String(e?.message || e) }
|
|
148
162
|
}
|
|
@@ -161,12 +175,12 @@ async function optimizeVideo (file, { onProgress } = {}) {
|
|
|
161
175
|
return { changed: true, file: newFile }
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
async function selectVideoEncoderConfig ({ width, height, fps }) {
|
|
165
|
-
const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, bitrate
|
|
178
|
+
async function selectVideoEncoderConfig ({ width, height, fps, bitrate = TARGET_VIDEO_BITRATE }) {
|
|
179
|
+
const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', hevc: { format: 'hevc' } }
|
|
166
180
|
const supH = await VideoEncoder.isConfigSupported(hevc).catch(() => ({ supported: false }))
|
|
167
181
|
if (supH.supported) return { codecId: 'hevc', config: supH.config }
|
|
168
182
|
|
|
169
|
-
const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate
|
|
183
|
+
const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
|
|
170
184
|
const supA = await VideoEncoder.isConfigSupported(avc)
|
|
171
185
|
return { codecId: 'avc', config: supA.config }
|
|
172
186
|
}
|
|
@@ -175,6 +189,12 @@ function shouldDecodeViaVideoElement () {
|
|
|
175
189
|
return (navigator?.vendor || '').includes('Apple')
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
async function applyVideoEncoderBackpressure (encoder) {
|
|
193
|
+
while (encoder.encodeQueueSize > MAX_VIDEO_ENCODER_QUEUE_SIZE) {
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
async function waitForFrameReady (video, budgetMs) {
|
|
179
199
|
if (typeof video.requestVideoFrameCallback !== 'function') return false
|
|
180
200
|
return await new Promise((resolve) => {
|
|
@@ -239,6 +259,7 @@ async function encodeFramesViaVideoElement ({ file, durationCfr, step, frames, c
|
|
|
239
259
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
240
260
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
241
261
|
vf.close()
|
|
262
|
+
await applyVideoEncoderBackpressure(ve)
|
|
242
263
|
|
|
243
264
|
if (typeof onProgress === 'function') {
|
|
244
265
|
try {
|
|
@@ -285,6 +306,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
|
|
|
285
306
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
286
307
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
287
308
|
vf.close()
|
|
309
|
+
await applyVideoEncoderBackpressure(ve)
|
|
288
310
|
|
|
289
311
|
if (typeof onProgress === 'function') {
|
|
290
312
|
try {
|
|
@@ -311,6 +333,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
|
|
|
311
333
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
312
334
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
313
335
|
vf.close()
|
|
336
|
+
await applyVideoEncoderBackpressure(ve)
|
|
314
337
|
|
|
315
338
|
if (typeof onProgress === 'function') {
|
|
316
339
|
try {
|
|
@@ -628,12 +651,13 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
|
|
|
628
651
|
const targetWidth = Math.max(2, Number(plan?.width) || Math.round(w * scale))
|
|
629
652
|
const targetHeight = Math.max(2, Number(plan?.height) || Math.round(h * scale))
|
|
630
653
|
|
|
631
|
-
const
|
|
654
|
+
const fallbackPlan = plan || await determineEncodingPlan(file, { width: w, height: h })
|
|
655
|
+
const targetFps = Math.max(1, Number(fallbackPlan.fps))
|
|
632
656
|
const step = 1 / Math.max(1, targetFps)
|
|
633
657
|
const frames = Math.max(1, Math.floor(durationCfr / step))
|
|
634
658
|
|
|
635
659
|
const output = new Output({ format: new Mp4OutputFormat({ fastStart: 'in-memory' }), target: new BufferTarget() })
|
|
636
|
-
const { codecId, config: usedCfg } = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps: targetFps })
|
|
660
|
+
const { codecId, config: usedCfg } = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps: targetFps, bitrate: fallbackPlan.bitrate })
|
|
637
661
|
const videoTrack = new EncodedVideoPacketSource(codecId)
|
|
638
662
|
output.addVideoTrack(videoTrack, { frameRate: targetFps })
|
|
639
663
|
|
data/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const TARGET_VIDEO_BITRATE = 12_000_000
|
|
|
13
13
|
const TARGET_AUDIO_BITRATE = 96_000
|
|
14
14
|
const TARGET_AUDIO_SR = 48_000
|
|
15
15
|
const TARGET_AUDIO_CHANNELS = 2
|
|
16
|
+
const MAX_VIDEO_ENCODER_QUEUE_SIZE = 4
|
|
16
17
|
|
|
17
18
|
// ----- Video metadata probe -----
|
|
18
19
|
async function probeVideo (file) {
|
|
@@ -33,34 +34,47 @@ async function probeVideo (file) {
|
|
|
33
34
|
})
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function
|
|
37
|
+
async function estimateSourceVideoStats (file) {
|
|
37
38
|
try {
|
|
38
39
|
const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS })
|
|
39
40
|
const tracks = await input.getTracks()
|
|
40
41
|
const video = tracks.find(t => typeof t.isVideoTrack === 'function' && t.isVideoTrack())
|
|
41
|
-
if (!video) return 0
|
|
42
|
+
if (!video) return { fps: 0, bitrate: 0 }
|
|
42
43
|
const sink = new EncodedPacketSink(video)
|
|
43
44
|
const durations = []
|
|
45
|
+
let firstTimestamp = Infinity
|
|
46
|
+
let lastTimestamp = -Infinity
|
|
47
|
+
let totalBytes = 0
|
|
44
48
|
for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
|
|
45
|
-
const
|
|
46
|
-
if (packet.timestamp >= 0 && Number.isFinite(
|
|
49
|
+
const duration = Number(packet?.duration)
|
|
50
|
+
if (packet.timestamp >= 0 && Number.isFinite(duration) && duration > 0) {
|
|
51
|
+
durations.push(duration)
|
|
52
|
+
firstTimestamp = Math.min(firstTimestamp, packet.timestamp)
|
|
53
|
+
lastTimestamp = Math.max(lastTimestamp, packet.timestamp + duration)
|
|
54
|
+
totalBytes += packet.byteLength
|
|
55
|
+
}
|
|
47
56
|
if (durations.length >= 120) break
|
|
48
57
|
}
|
|
49
|
-
if (!durations.length) return 0
|
|
58
|
+
if (!durations.length) return { fps: 0, bitrate: 0 }
|
|
50
59
|
durations.sort((a, b) => a - b)
|
|
51
|
-
const
|
|
52
|
-
|
|
60
|
+
const duration = durations[Math.floor(durations.length / 2)]
|
|
61
|
+
const sampledDuration = lastTimestamp - firstTimestamp
|
|
62
|
+
return {
|
|
63
|
+
fps: Number.isFinite(duration) && duration > 0 ? (1 / duration) : 0,
|
|
64
|
+
bitrate: sampledDuration > 0 ? (totalBytes * 8 / sampledDuration) : 0
|
|
65
|
+
}
|
|
53
66
|
} catch (_) {
|
|
54
|
-
return 0
|
|
67
|
+
return { fps: 0, bitrate: 0 }
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
async function
|
|
71
|
+
async function determineEncodingPlan (file, { width, height }) {
|
|
59
72
|
const maxFps = Math.max(width, height) <= 1920 ? 30 : 60
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
const source = await estimateSourceVideoStats(file)
|
|
74
|
+
return {
|
|
75
|
+
fps: maxFps === 30 ? 30 : (source.fps >= 45 ? 60 : 30),
|
|
76
|
+
bitrate: source.bitrate > 0 ? Math.min(TARGET_VIDEO_BITRATE, Math.round(source.bitrate)) : TARGET_VIDEO_BITRATE
|
|
77
|
+
}
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
// ----- Audio helpers -----
|
|
@@ -124,8 +138,8 @@ async function canOptimizeVideo (file) {
|
|
|
124
138
|
const scale = Math.min(1, MAX_LONG_SIDE / Math.max(2, long))
|
|
125
139
|
const targetWidth = Math.max(2, Math.round(width * scale))
|
|
126
140
|
const targetHeight = Math.max(2, Math.round(height * scale))
|
|
127
|
-
const
|
|
128
|
-
const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight,
|
|
141
|
+
const plan = await determineEncodingPlan(file, { width, height })
|
|
142
|
+
const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, ...plan }).then(() => true).catch(() => false)
|
|
129
143
|
if (!sup) return { ok: false, reason: 'unsupported-video-config', message: 'No supported encoder configuration for this resolution on this device.' }
|
|
130
144
|
|
|
131
145
|
// Header sniffing when file.type is empty/incorrect
|
|
@@ -141,7 +155,7 @@ async function canOptimizeVideo (file) {
|
|
|
141
155
|
const hasEbml = buf.length >= 4 && buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3
|
|
142
156
|
if (!(hasFtyp || hasEbml)) return { ok: false, reason: 'unknown-container', message: 'Unrecognized container; expected MP4/MOV or WebM.' }
|
|
143
157
|
}
|
|
144
|
-
return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight,
|
|
158
|
+
return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, ...plan } }
|
|
145
159
|
} catch (e) {
|
|
146
160
|
return { ok: false, reason: 'probe-failed', message: String(e?.message || e) }
|
|
147
161
|
}
|
|
@@ -160,12 +174,12 @@ async function optimizeVideo (file, { onProgress } = {}) {
|
|
|
160
174
|
return { changed: true, file: newFile }
|
|
161
175
|
}
|
|
162
176
|
|
|
163
|
-
async function selectVideoEncoderConfig ({ width, height, fps }) {
|
|
164
|
-
const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, bitrate
|
|
177
|
+
async function selectVideoEncoderConfig ({ width, height, fps, bitrate = TARGET_VIDEO_BITRATE }) {
|
|
178
|
+
const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', hevc: { format: 'hevc' } }
|
|
165
179
|
const supH = await VideoEncoder.isConfigSupported(hevc).catch(() => ({ supported: false }))
|
|
166
180
|
if (supH.supported) return { codecId: 'hevc', config: supH.config }
|
|
167
181
|
|
|
168
|
-
const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate
|
|
182
|
+
const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
|
|
169
183
|
const supA = await VideoEncoder.isConfigSupported(avc)
|
|
170
184
|
return { codecId: 'avc', config: supA.config }
|
|
171
185
|
}
|
|
@@ -174,6 +188,12 @@ function shouldDecodeViaVideoElement () {
|
|
|
174
188
|
return (navigator?.vendor || '').includes('Apple')
|
|
175
189
|
}
|
|
176
190
|
|
|
191
|
+
async function applyVideoEncoderBackpressure (encoder) {
|
|
192
|
+
while (encoder.encodeQueueSize > MAX_VIDEO_ENCODER_QUEUE_SIZE) {
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
async function waitForFrameReady (video, budgetMs) {
|
|
178
198
|
if (typeof video.requestVideoFrameCallback !== 'function') return false
|
|
179
199
|
return await new Promise((resolve) => {
|
|
@@ -238,6 +258,7 @@ async function encodeFramesViaVideoElement ({ file, durationCfr, step, frames, c
|
|
|
238
258
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
239
259
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
240
260
|
vf.close()
|
|
261
|
+
await applyVideoEncoderBackpressure(ve)
|
|
241
262
|
|
|
242
263
|
if (typeof onProgress === 'function') {
|
|
243
264
|
try {
|
|
@@ -284,6 +305,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
|
|
|
284
305
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
285
306
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
286
307
|
vf.close()
|
|
308
|
+
await applyVideoEncoderBackpressure(ve)
|
|
287
309
|
|
|
288
310
|
if (typeof onProgress === 'function') {
|
|
289
311
|
try {
|
|
@@ -310,6 +332,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
|
|
|
310
332
|
const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
|
|
311
333
|
ve.encode(vf, { keyFrame: i === 0 })
|
|
312
334
|
vf.close()
|
|
335
|
+
await applyVideoEncoderBackpressure(ve)
|
|
313
336
|
|
|
314
337
|
if (typeof onProgress === 'function') {
|
|
315
338
|
try {
|
|
@@ -627,12 +650,13 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
|
|
|
627
650
|
const targetWidth = Math.max(2, Number(plan?.width) || Math.round(w * scale))
|
|
628
651
|
const targetHeight = Math.max(2, Number(plan?.height) || Math.round(h * scale))
|
|
629
652
|
|
|
630
|
-
const
|
|
653
|
+
const fallbackPlan = plan || await determineEncodingPlan(file, { width: w, height: h })
|
|
654
|
+
const targetFps = Math.max(1, Number(fallbackPlan.fps))
|
|
631
655
|
const step = 1 / Math.max(1, targetFps)
|
|
632
656
|
const frames = Math.max(1, Math.floor(durationCfr / step))
|
|
633
657
|
|
|
634
658
|
const output = new Output({ format: new Mp4OutputFormat({ fastStart: 'in-memory' }), target: new BufferTarget() })
|
|
635
|
-
const { codecId, config: usedCfg } = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps: targetFps })
|
|
659
|
+
const { codecId, config: usedCfg } = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps: targetFps, bitrate: fallbackPlan.bitrate })
|
|
636
660
|
const videoTrack = new EncodedVideoPacketSource(codecId)
|
|
637
661
|
output.addVideoTrack(videoTrack, { frameRate: targetFps })
|
|
638
662
|
|
data/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "straight-to-video",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "straight-to-video",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.12",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"mediabunny": "^1.27.3"
|
data/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "straight-to-video",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Browser-based, hardware-accelerated video upload optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/searlsco/straight-to-video.git"
|
|
29
|
+
"url": "git+https://github.com/searlsco/straight-to-video.git"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"files": [
|