straight_to_video 0.0.10 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb365dda5fa968b1cae50f89065e24b0d49f49fa911aa067ace2b19052243123
4
- data.tar.gz: a67e3ac69b986d0a4458d8c72412ef52de6deb4f564312a3bf156199a99f5260
3
+ metadata.gz: c562c9b7243f141e7524a58e115bd257d5207bd7272bf01547812e9845235e75
4
+ data.tar.gz: bbe4b5f3e82e6d84fb928c9165d5c8935416ed6c04034c55db44c740db949785
5
5
  SHA512:
6
- metadata.gz: c0da985ed8833ee888047ea9654df6d2fc964bf5629a11c03bfdd292047a183f59a41b5fd127805eb25731ff8b34171969b3a36c57ac40bd95961f5c1628ab54
7
- data.tar.gz: 315f2daf669d9b524f6310f1796becc0606573a39b85f93cf1a725c6e6eceacaf7c2b6bee4cab398f5e3261d05ace06fc42c800095cb14197d26066846bf6f2d
6
+ metadata.gz: 368159b6977e3d37b116a55a35cca1d849029657ae86b01aa8113f9385288dab86f140d98a5bbb4d9639464e7e2ef8c362e204b52c45e5e2c36927e9aaa4db93
7
+ data.tar.gz: 8acd9f7af6b1293e7b2a6bd4626e6dc0897a09811684bcf8a7e27260dd48071306c1aba8b61c6b8d9a76f1aac0f5552eb8edd43f0e7e9cc9259e800531a3803d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
8
+ ## 0.0.11
9
+
10
+ * Work around WebKit labeling the requested first HEVC keyframe as a delta packet.
11
+ * Keep browser-encoded video below the 25 Mbps delivery limit.
12
+ * Accept WebKit's full-range `yuvj420p` output as progressive 4:2:0 video.
13
+
3
14
  ## 0.0.10
4
15
 
5
16
  - Fix Safari macOS Tahoe bug where native video controls never auto-hide by rewriting the moov atom to match ffmpeg's conventions (zero timestamps, standard handler names, edts/elst, extended esds, btrt, sgpd/sbgp)
@@ -1,4 +1,4 @@
1
- // straight-to-video@0.0.10 vendored by the straight_to_video gem
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 -----
@@ -10,9 +10,11 @@ import {
10
10
 
11
11
  // ----- Constants -----
12
12
  const MAX_LONG_SIDE = 1920
13
+ const TARGET_VIDEO_BITRATE = 12_000_000
13
14
  const TARGET_AUDIO_BITRATE = 96_000
14
15
  const TARGET_AUDIO_SR = 48_000
15
16
  const TARGET_AUDIO_CHANNELS = 2
17
+ const MAX_VIDEO_ENCODER_QUEUE_SIZE = 4
16
18
 
17
19
  // ----- Video metadata probe -----
18
20
  async function probeVideo (file) {
@@ -33,34 +35,47 @@ async function probeVideo (file) {
33
35
  })
34
36
  }
35
37
 
36
- async function estimateSourceVideoFps (file) {
38
+ async function estimateSourceVideoStats (file) {
37
39
  try {
38
40
  const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS })
39
41
  const tracks = await input.getTracks()
40
42
  const video = tracks.find(t => typeof t.isVideoTrack === 'function' && t.isVideoTrack())
41
- if (!video) return 0
43
+ if (!video) return { fps: 0, bitrate: 0 }
42
44
  const sink = new EncodedPacketSink(video)
43
45
  const durations = []
46
+ let firstTimestamp = Infinity
47
+ let lastTimestamp = -Infinity
48
+ let totalBytes = 0
44
49
  for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
45
- const dur = Number(packet?.duration)
46
- if (packet.timestamp >= 0 && Number.isFinite(dur) && dur > 0) durations.push(dur)
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
+ }
47
57
  if (durations.length >= 120) break
48
58
  }
49
- if (!durations.length) return 0
59
+ if (!durations.length) return { fps: 0, bitrate: 0 }
50
60
  durations.sort((a, b) => a - b)
51
- const dur = durations[Math.floor(durations.length / 2)]
52
- return Number.isFinite(dur) && dur > 0 ? (1 / dur) : 0
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
+ }
53
67
  } catch (_) {
54
- return 0
68
+ return { fps: 0, bitrate: 0 }
55
69
  }
56
70
  }
57
71
 
58
- async function determineTargetFps (file, { width, height }) {
72
+ async function determineEncodingPlan (file, { width, height }) {
59
73
  const maxFps = Math.max(width, height) <= 1920 ? 30 : 60
60
- if (maxFps === 30) return 30
61
-
62
- const fps = await estimateSourceVideoFps(file)
63
- return fps >= 45 ? 60 : 30
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
+ }
64
79
  }
65
80
 
66
81
  // ----- Audio helpers -----
@@ -124,8 +139,8 @@ async function canOptimizeVideo (file) {
124
139
  const scale = Math.min(1, MAX_LONG_SIDE / Math.max(2, long))
125
140
  const targetWidth = Math.max(2, Math.round(width * scale))
126
141
  const targetHeight = Math.max(2, Math.round(height * scale))
127
- const fps = await determineTargetFps(file, { width, height })
128
- const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps }).then(() => true).catch(() => false)
142
+ const plan = await determineEncodingPlan(file, { width, height })
143
+ const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, ...plan }).then(() => true).catch(() => false)
129
144
  if (!sup) return { ok: false, reason: 'unsupported-video-config', message: 'No supported encoder configuration for this resolution on this device.' }
130
145
 
131
146
  // Header sniffing when file.type is empty/incorrect
@@ -141,7 +156,7 @@ async function canOptimizeVideo (file) {
141
156
  const hasEbml = buf.length >= 4 && buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3
142
157
  if (!(hasFtyp || hasEbml)) return { ok: false, reason: 'unknown-container', message: 'Unrecognized container; expected MP4/MOV or WebM.' }
143
158
  }
144
- return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, fps } }
159
+ return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, ...plan } }
145
160
  } catch (e) {
146
161
  return { ok: false, reason: 'probe-failed', message: String(e?.message || e) }
147
162
  }
@@ -160,12 +175,12 @@ async function optimizeVideo (file, { onProgress } = {}) {
160
175
  return { changed: true, file: newFile }
161
176
  }
162
177
 
163
- async function selectVideoEncoderConfig ({ width, height, fps }) {
164
- const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, hardwareAcceleration: 'prefer-hardware', hevc: { format: 'hevc' } }
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' } }
165
180
  const supH = await VideoEncoder.isConfigSupported(hevc).catch(() => ({ supported: false }))
166
181
  if (supH.supported) return { codecId: 'hevc', config: supH.config }
167
182
 
168
- const avc = { codec: 'avc1.64002A', width, height, framerate: fps, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
183
+ const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
169
184
  const supA = await VideoEncoder.isConfigSupported(avc)
170
185
  return { codecId: 'avc', config: supA.config }
171
186
  }
@@ -174,6 +189,12 @@ function shouldDecodeViaVideoElement () {
174
189
  return (navigator?.vendor || '').includes('Apple')
175
190
  }
176
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
+
177
198
  async function waitForFrameReady (video, budgetMs) {
178
199
  if (typeof video.requestVideoFrameCallback !== 'function') return false
179
200
  return await new Promise((resolve) => {
@@ -238,6 +259,7 @@ async function encodeFramesViaVideoElement ({ file, durationCfr, step, frames, c
238
259
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
239
260
  ve.encode(vf, { keyFrame: i === 0 })
240
261
  vf.close()
262
+ await applyVideoEncoderBackpressure(ve)
241
263
 
242
264
  if (typeof onProgress === 'function') {
243
265
  try {
@@ -284,6 +306,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
284
306
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
285
307
  ve.encode(vf, { keyFrame: i === 0 })
286
308
  vf.close()
309
+ await applyVideoEncoderBackpressure(ve)
287
310
 
288
311
  if (typeof onProgress === 'function') {
289
312
  try {
@@ -310,6 +333,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
310
333
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
311
334
  ve.encode(vf, { keyFrame: i === 0 })
312
335
  vf.close()
336
+ await applyVideoEncoderBackpressure(ve)
313
337
 
314
338
  if (typeof onProgress === 'function') {
315
339
  try {
@@ -627,12 +651,13 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
627
651
  const targetWidth = Math.max(2, Number(plan?.width) || Math.round(w * scale))
628
652
  const targetHeight = Math.max(2, Number(plan?.height) || Math.round(h * scale))
629
653
 
630
- const targetFps = Math.max(1, Number(plan?.fps) || await determineTargetFps(file, { width: w, height: h }))
654
+ const fallbackPlan = plan || await determineEncodingPlan(file, { width: w, height: h })
655
+ const targetFps = Math.max(1, Number(fallbackPlan.fps))
631
656
  const step = 1 / Math.max(1, targetFps)
632
657
  const frames = Math.max(1, Math.floor(durationCfr / step))
633
658
 
634
659
  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 })
660
+ const { codecId, config: usedCfg } = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps: targetFps, bitrate: fallbackPlan.bitrate })
636
661
  const videoTrack = new EncodedVideoPacketSource(codecId)
637
662
  output.addVideoTrack(videoTrack, { frameRate: targetFps })
638
663
 
@@ -685,7 +710,7 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
685
710
  const { chunk } = pendingPackets[i]
686
711
  const data = new Uint8Array(chunk.byteLength); chunk.copyTo(data)
687
712
  const ts = i * step; const dur = step
688
- const pkt = new EncodedPacket(data, chunk.type === 'key' ? 'key' : 'delta', ts, dur)
713
+ const pkt = new EncodedPacket(data, i === 0 || chunk.type === 'key' ? 'key' : 'delta', ts, dur)
689
714
  await videoTrack.add(pkt, { decoderConfig: { codec: usedCfg.codec, codedWidth: targetWidth, codedHeight: targetHeight, description: codecDesc } })
690
715
  }
691
716
 
data/index.js CHANGED
@@ -9,9 +9,11 @@ import {
9
9
 
10
10
  // ----- Constants -----
11
11
  const MAX_LONG_SIDE = 1920
12
+ const TARGET_VIDEO_BITRATE = 12_000_000
12
13
  const TARGET_AUDIO_BITRATE = 96_000
13
14
  const TARGET_AUDIO_SR = 48_000
14
15
  const TARGET_AUDIO_CHANNELS = 2
16
+ const MAX_VIDEO_ENCODER_QUEUE_SIZE = 4
15
17
 
16
18
  // ----- Video metadata probe -----
17
19
  async function probeVideo (file) {
@@ -32,34 +34,47 @@ async function probeVideo (file) {
32
34
  })
33
35
  }
34
36
 
35
- async function estimateSourceVideoFps (file) {
37
+ async function estimateSourceVideoStats (file) {
36
38
  try {
37
39
  const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS })
38
40
  const tracks = await input.getTracks()
39
41
  const video = tracks.find(t => typeof t.isVideoTrack === 'function' && t.isVideoTrack())
40
- if (!video) return 0
42
+ if (!video) return { fps: 0, bitrate: 0 }
41
43
  const sink = new EncodedPacketSink(video)
42
44
  const durations = []
45
+ let firstTimestamp = Infinity
46
+ let lastTimestamp = -Infinity
47
+ let totalBytes = 0
43
48
  for await (const packet of sink.packets(undefined, undefined, { metadataOnly: true })) {
44
- const dur = Number(packet?.duration)
45
- if (packet.timestamp >= 0 && Number.isFinite(dur) && dur > 0) durations.push(dur)
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
+ }
46
56
  if (durations.length >= 120) break
47
57
  }
48
- if (!durations.length) return 0
58
+ if (!durations.length) return { fps: 0, bitrate: 0 }
49
59
  durations.sort((a, b) => a - b)
50
- const dur = durations[Math.floor(durations.length / 2)]
51
- return Number.isFinite(dur) && dur > 0 ? (1 / dur) : 0
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
+ }
52
66
  } catch (_) {
53
- return 0
67
+ return { fps: 0, bitrate: 0 }
54
68
  }
55
69
  }
56
70
 
57
- async function determineTargetFps (file, { width, height }) {
71
+ async function determineEncodingPlan (file, { width, height }) {
58
72
  const maxFps = Math.max(width, height) <= 1920 ? 30 : 60
59
- if (maxFps === 30) return 30
60
-
61
- const fps = await estimateSourceVideoFps(file)
62
- return fps >= 45 ? 60 : 30
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
+ }
63
78
  }
64
79
 
65
80
  // ----- Audio helpers -----
@@ -123,8 +138,8 @@ async function canOptimizeVideo (file) {
123
138
  const scale = Math.min(1, MAX_LONG_SIDE / Math.max(2, long))
124
139
  const targetWidth = Math.max(2, Math.round(width * scale))
125
140
  const targetHeight = Math.max(2, Math.round(height * scale))
126
- const fps = await determineTargetFps(file, { width, height })
127
- const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, fps }).then(() => true).catch(() => false)
141
+ const plan = await determineEncodingPlan(file, { width, height })
142
+ const sup = await selectVideoEncoderConfig({ width: targetWidth, height: targetHeight, ...plan }).then(() => true).catch(() => false)
128
143
  if (!sup) return { ok: false, reason: 'unsupported-video-config', message: 'No supported encoder configuration for this resolution on this device.' }
129
144
 
130
145
  // Header sniffing when file.type is empty/incorrect
@@ -140,7 +155,7 @@ async function canOptimizeVideo (file) {
140
155
  const hasEbml = buf.length >= 4 && buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3
141
156
  if (!(hasFtyp || hasEbml)) return { ok: false, reason: 'unknown-container', message: 'Unrecognized container; expected MP4/MOV or WebM.' }
142
157
  }
143
- return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, fps } }
158
+ return { ok: true, reason: 'ok', message: 'ok', plan: { width: targetWidth, height: targetHeight, ...plan } }
144
159
  } catch (e) {
145
160
  return { ok: false, reason: 'probe-failed', message: String(e?.message || e) }
146
161
  }
@@ -159,12 +174,12 @@ async function optimizeVideo (file, { onProgress } = {}) {
159
174
  return { changed: true, file: newFile }
160
175
  }
161
176
 
162
- async function selectVideoEncoderConfig ({ width, height, fps }) {
163
- const hevc = { codec: 'hvc1.1.4.L123.B0', width, height, framerate: fps, hardwareAcceleration: 'prefer-hardware', hevc: { format: 'hevc' } }
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' } }
164
179
  const supH = await VideoEncoder.isConfigSupported(hevc).catch(() => ({ supported: false }))
165
180
  if (supH.supported) return { codecId: 'hevc', config: supH.config }
166
181
 
167
- const avc = { codec: 'avc1.64002A', width, height, framerate: fps, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
182
+ const avc = { codec: 'avc1.64002A', width, height, framerate: fps, bitrate, hardwareAcceleration: 'prefer-hardware', avc: { format: 'avc' } }
168
183
  const supA = await VideoEncoder.isConfigSupported(avc)
169
184
  return { codecId: 'avc', config: supA.config }
170
185
  }
@@ -173,6 +188,12 @@ function shouldDecodeViaVideoElement () {
173
188
  return (navigator?.vendor || '').includes('Apple')
174
189
  }
175
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
+
176
197
  async function waitForFrameReady (video, budgetMs) {
177
198
  if (typeof video.requestVideoFrameCallback !== 'function') return false
178
199
  return await new Promise((resolve) => {
@@ -237,6 +258,7 @@ async function encodeFramesViaVideoElement ({ file, durationCfr, step, frames, c
237
258
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
238
259
  ve.encode(vf, { keyFrame: i === 0 })
239
260
  vf.close()
261
+ await applyVideoEncoderBackpressure(ve)
240
262
 
241
263
  if (typeof onProgress === 'function') {
242
264
  try {
@@ -283,6 +305,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
283
305
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
284
306
  ve.encode(vf, { keyFrame: i === 0 })
285
307
  vf.close()
308
+ await applyVideoEncoderBackpressure(ve)
286
309
 
287
310
  if (typeof onProgress === 'function') {
288
311
  try {
@@ -309,6 +332,7 @@ async function encodeFramesViaVideoSampleSink ({ file, durationCfr, step, frames
309
332
  const vf = new VideoFrame(canvas, { timestamp: Math.round(t * 1e6), duration: Math.round(step * 1e6) })
310
333
  ve.encode(vf, { keyFrame: i === 0 })
311
334
  vf.close()
335
+ await applyVideoEncoderBackpressure(ve)
312
336
 
313
337
  if (typeof onProgress === 'function') {
314
338
  try {
@@ -626,12 +650,13 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
626
650
  const targetWidth = Math.max(2, Number(plan?.width) || Math.round(w * scale))
627
651
  const targetHeight = Math.max(2, Number(plan?.height) || Math.round(h * scale))
628
652
 
629
- const targetFps = Math.max(1, Number(plan?.fps) || await determineTargetFps(file, { width: w, height: h }))
653
+ const fallbackPlan = plan || await determineEncodingPlan(file, { width: w, height: h })
654
+ const targetFps = Math.max(1, Number(fallbackPlan.fps))
630
655
  const step = 1 / Math.max(1, targetFps)
631
656
  const frames = Math.max(1, Math.floor(durationCfr / step))
632
657
 
633
658
  const output = new Output({ format: new Mp4OutputFormat({ fastStart: 'in-memory' }), target: new BufferTarget() })
634
- 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 })
635
660
  const videoTrack = new EncodedVideoPacketSource(codecId)
636
661
  output.addVideoTrack(videoTrack, { frameRate: targetFps })
637
662
 
@@ -684,7 +709,7 @@ async function encodeVideo ({ file, srcMeta, plan, onProgress }) {
684
709
  const { chunk } = pendingPackets[i]
685
710
  const data = new Uint8Array(chunk.byteLength); chunk.copyTo(data)
686
711
  const ts = i * step; const dur = step
687
- const pkt = new EncodedPacket(data, chunk.type === 'key' ? 'key' : 'delta', ts, dur)
712
+ const pkt = new EncodedPacket(data, i === 0 || chunk.type === 'key' ? 'key' : 'delta', ts, dur)
688
713
  await videoTrack.add(pkt, { decoderConfig: { codec: usedCfg.codec, codedWidth: targetWidth, codedHeight: targetHeight, description: codecDesc } })
689
714
  }
690
715
 
@@ -1,3 +1,3 @@
1
1
  module StraightToVideo
2
- VERSION = "0.0.10"
2
+ VERSION = "0.0.12"
3
3
  end
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "straight-to-video",
3
- "version": "0.0.10",
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.10",
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.10",
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": [
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: straight_to_video
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
77
  - !ruby/object:Gem::Version
78
78
  version: '0'
79
79
  requirements: []
80
- rubygems_version: 4.0.8
80
+ rubygems_version: 3.6.9
81
81
  specification_version: 4
82
82
  summary: Browser-based, hardware-accelerated video upload optimization
83
83
  test_files: []