straight_to_video 0.0.5 → 0.0.7

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: 7f9374ddbb488a573260822bdfc2b8a5a183f0c1f8c10e4e6428b7ae8c3e7f86
4
- data.tar.gz: 3fb96670c80e91c46c0214d4153e2a28b96d3f5cddb368875568978bf2421912
3
+ metadata.gz: 7412c0ad6de37671f90aad3c474a2dfc2f846e656de524248791ec318ad598b7
4
+ data.tar.gz: 4032432057ff6b71831f3e509e4300f34a8f662c70b977923ec60edfce2d15ae
5
5
  SHA512:
6
- metadata.gz: 54b446d8179dd83c5604902b741c15c07b388b0fbada671527faf07441d11d73af5c1ba6729128897b43cf2232b7f9dc8f8271e0037362a94155736a28309089
7
- data.tar.gz: bbe19415c4fa54b522fefbcf8aaaa208d72674aacf6e29db2830c06190076b45955b04a4e51ad75baa161abfe1ce1feb1a075970c439a863158b88459cfe26e1
6
+ metadata.gz: 6c1d31ff446323e0304df7abfa4d02d7a52b883149a97f810753b55944447e5001a9ec810fa37525a8a0e5be7662c552df5c70d740d82b8ba367389c2d86d1ac
7
+ data.tar.gz: d8244161a7cd07734f24f7bf24037131db2c7ccaceb54e89330474edc575d00c32d82b24317ae24561e9bd42f1191b06d986c19ae770e829f81ef973598cc330
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.7
4
+
5
+ - guard against a race condition for forms that subscribe to change events but don't disable submission while optimize is already underway
6
+
7
+ ## 0.0.6
8
+
9
+ - fix iOS Safari Stimulus controller hang by making `seeked` waits robust
10
+
3
11
  ## 0.0.5
4
12
 
5
13
  - unscrew up the extension in the importmap 🤦‍♂️
@@ -1,4 +1,4 @@
1
- // straight-to-video@0.0.5 vendored by the straight_to_video gem
1
+ // straight-to-video@0.0.7 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 -----
@@ -149,6 +149,20 @@ async function waitForFrameReady (video, budgetMs) {
149
149
  })
150
150
  }
151
151
 
152
+ async function seekOnce (video, time) {
153
+ if (!video) return
154
+ const t = Number.isFinite(time) ? time : 0
155
+ if (Math.abs(video.currentTime - t) < 1e-6) return
156
+ await new Promise((resolve) => {
157
+ const onSeeked = () => {
158
+ video.removeEventListener('seeked', onSeeked)
159
+ resolve()
160
+ }
161
+ video.addEventListener('seeked', onSeeked, { once: true })
162
+ video.currentTime = t
163
+ })
164
+ }
165
+
152
166
  async function encodeVideo ({ file, srcMeta, onProgress }) {
153
167
  const w = srcMeta.w
154
168
  const h = srcMeta.h
@@ -205,8 +219,26 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
205
219
  const url = URL.createObjectURL(file)
206
220
  const v = document.createElement('video')
207
221
  v.muted = true; v.preload = 'auto'; v.playsInline = true
208
- v.src = url
209
- await new Promise((resolve, reject) => { v.onloadedmetadata = resolve; v.onerror = () => reject(new Error('video load failed')) })
222
+ await new Promise((resolve, reject) => {
223
+ const onLoaded = () => {
224
+ v.removeEventListener('loadedmetadata', onLoaded)
225
+ v.removeEventListener('error', onError)
226
+ resolve()
227
+ }
228
+ const onError = () => {
229
+ v.removeEventListener('loadedmetadata', onLoaded)
230
+ v.removeEventListener('error', onError)
231
+ reject(new Error('video load failed'))
232
+ }
233
+ v.addEventListener('loadedmetadata', onLoaded)
234
+ v.addEventListener('error', onError)
235
+ v.src = url
236
+ try {
237
+ v.load()
238
+ } catch (err) {
239
+ console.warn('straight-to-video: video.load() threw; continuing without explicit load()', err)
240
+ }
241
+ })
210
242
  const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight
211
243
  const ctx = canvas.getContext('2d', { alpha: false })
212
244
 
@@ -216,13 +248,13 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
216
248
  const drawTime = i === 0
217
249
  ? Math.min(Math.max(0, t + (step * 0.5)), Math.max(0.000001, durationCfr - 0.000001))
218
250
  : targetTime
219
-
220
- await new Promise((resolve) => { v.currentTime = drawTime; v.onseeked = () => resolve() })
251
+ await seekOnce(v, drawTime)
221
252
  const budgetMs = Math.min(34, Math.max(17, Math.round(step * 1000)))
222
253
  const presented = await waitForFrameReady(v, budgetMs)
223
254
  if (!presented && i === 0) {
224
255
  const nudge = Math.min(step * 0.25, 0.004)
225
- await new Promise((resolve) => { v.currentTime = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001)); v.onseeked = () => resolve() })
256
+ const target = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001))
257
+ await seekOnce(v, target)
226
258
  }
227
259
 
228
260
  ctx.drawImage(v, 0, 0, canvas.width, canvas.height)
@@ -231,7 +263,11 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
231
263
  vf.close()
232
264
 
233
265
  if (typeof onProgress === 'function') {
234
- try { onProgress(Math.min(1, (i + 1) / frames)) } catch (_) {}
266
+ try {
267
+ onProgress(Math.min(1, (i + 1) / frames))
268
+ } catch (err) {
269
+ console.warn('straight-to-video: onProgress callback threw; ignoring error', err)
270
+ }
235
271
  }
236
272
  }
237
273
  await ve.flush()
@@ -255,7 +291,6 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
255
291
  const sample = new AudioSample({ format: 'f32', sampleRate: TARGET_AUDIO_SR, numberOfChannels: TARGET_AUDIO_CHANNELS, timestamp: 0, data: interleaved })
256
292
  await audioSource.add(sample)
257
293
  audioSource.close()
258
-
259
294
  await output.finalize()
260
295
  const { buffer } = output.target
261
296
  const payload = new Uint8Array(buffer)
@@ -323,6 +358,11 @@ function registerStraightToVideoController (app, opts = {}) {
323
358
  }
324
359
 
325
360
  async _processFileInput (fileInput) {
361
+ if (!this._pendingProcesses) this._pendingProcesses = new WeakMap()
362
+ const existing = this._pendingProcesses.get(fileInput)
363
+ if (existing) return existing
364
+
365
+ const job = (async () => {
326
366
  this._markFlag(fileInput, 'processing')
327
367
  fileInput.disabled = true
328
368
  try {
@@ -341,6 +381,13 @@ function registerStraightToVideoController (app, opts = {}) {
341
381
  fileInput.disabled = false
342
382
  this._unmarkFlag(fileInput, 'processing')
343
383
  }
384
+ })()
385
+
386
+ this._pendingProcesses.set(fileInput, job)
387
+ job.finally(() => {
388
+ if (this._pendingProcesses?.get(fileInput) === job) this._pendingProcesses.delete(fileInput)
389
+ })
390
+ return job
344
391
  }
345
392
 
346
393
  _fire (el, name, detail = {}) {
data/index.html CHANGED
@@ -169,8 +169,8 @@
169
169
 
170
170
  <section id="install">
171
171
  <h2>Install</h2>
172
- <pre class="language-bash"><code>npm install straight-to-video</code></pre>
173
- <pre class="language-bash"><code>bundle add straight_to_video</code></pre>
172
+ <pre class="language-bash"><code>npm install straight-to-video</code></pre>
173
+ <pre class="language-bash"><code>bundle add straight_to_video</code></pre>
174
174
  <p>
175
175
  <a class="btn ghost readme" href="https://github.com/searlsco/straight-to-video" target="_blank" rel="noopener">View README</a>
176
176
  </p>
@@ -179,7 +179,12 @@
179
179
 
180
180
  <script type="module">
181
181
  import { Application, Controller } from '@hotwired/stimulus'
182
- import { registerStraightToVideoController } from 'straight-to-video'
182
+ import { canOptimizeVideo, optimizeVideo, registerStraightToVideoController } from 'straight-to-video'
183
+
184
+ // Declare as global so that people can play with them
185
+ window.canOptimizeVideo = canOptimizeVideo
186
+ window.optimizeVideo = optimizeVideo
187
+ window.registerStraightToVideoController = registerStraightToVideoController
183
188
 
184
189
  const app = Application.start()
185
190
  registerStraightToVideoController(app, { Controller })
@@ -193,11 +198,16 @@
193
198
  const dl = document.getElementById('download')
194
199
  const sizes = document.getElementById('sizes')
195
200
 
196
- // Removed environment badge
197
-
198
201
  // Single‑button flow: open picker then auto‑submit
199
202
  tryBtn.addEventListener('click', () => {
200
- if (fileEl.showPicker) { try { fileEl.showPicker(); return } catch (_) {} }
203
+ if (fileEl.showPicker) {
204
+ try {
205
+ fileEl.showPicker()
206
+ return
207
+ } catch (err) {
208
+ console.warn('straight-to-video demo: file input showPicker() failed; falling back to click()', err)
209
+ }
210
+ }
201
211
  fileEl.click()
202
212
  })
203
213
 
@@ -208,7 +218,11 @@
208
218
  sizes.classList.remove('hidden')
209
219
  p.classList.remove('hidden')
210
220
  pct && (pct.textContent = '0%')
211
- try { form.requestSubmit() } catch (_) {}
221
+ try {
222
+ form.requestSubmit()
223
+ } catch (err) {
224
+ console.warn('straight-to-video demo: form.requestSubmit() failed; user may need to submit manually', err)
225
+ }
212
226
  })
213
227
 
214
228
  // Mirror controller events into the UI
data/index.js CHANGED
@@ -148,6 +148,20 @@ async function waitForFrameReady (video, budgetMs) {
148
148
  })
149
149
  }
150
150
 
151
+ async function seekOnce (video, time) {
152
+ if (!video) return
153
+ const t = Number.isFinite(time) ? time : 0
154
+ if (Math.abs(video.currentTime - t) < 1e-6) return
155
+ await new Promise((resolve) => {
156
+ const onSeeked = () => {
157
+ video.removeEventListener('seeked', onSeeked)
158
+ resolve()
159
+ }
160
+ video.addEventListener('seeked', onSeeked, { once: true })
161
+ video.currentTime = t
162
+ })
163
+ }
164
+
151
165
  async function encodeVideo ({ file, srcMeta, onProgress }) {
152
166
  const w = srcMeta.w
153
167
  const h = srcMeta.h
@@ -204,8 +218,26 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
204
218
  const url = URL.createObjectURL(file)
205
219
  const v = document.createElement('video')
206
220
  v.muted = true; v.preload = 'auto'; v.playsInline = true
207
- v.src = url
208
- await new Promise((resolve, reject) => { v.onloadedmetadata = resolve; v.onerror = () => reject(new Error('video load failed')) })
221
+ await new Promise((resolve, reject) => {
222
+ const onLoaded = () => {
223
+ v.removeEventListener('loadedmetadata', onLoaded)
224
+ v.removeEventListener('error', onError)
225
+ resolve()
226
+ }
227
+ const onError = () => {
228
+ v.removeEventListener('loadedmetadata', onLoaded)
229
+ v.removeEventListener('error', onError)
230
+ reject(new Error('video load failed'))
231
+ }
232
+ v.addEventListener('loadedmetadata', onLoaded)
233
+ v.addEventListener('error', onError)
234
+ v.src = url
235
+ try {
236
+ v.load()
237
+ } catch (err) {
238
+ console.warn('straight-to-video: video.load() threw; continuing without explicit load()', err)
239
+ }
240
+ })
209
241
  const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight
210
242
  const ctx = canvas.getContext('2d', { alpha: false })
211
243
 
@@ -215,13 +247,13 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
215
247
  const drawTime = i === 0
216
248
  ? Math.min(Math.max(0, t + (step * 0.5)), Math.max(0.000001, durationCfr - 0.000001))
217
249
  : targetTime
218
-
219
- await new Promise((resolve) => { v.currentTime = drawTime; v.onseeked = () => resolve() })
250
+ await seekOnce(v, drawTime)
220
251
  const budgetMs = Math.min(34, Math.max(17, Math.round(step * 1000)))
221
252
  const presented = await waitForFrameReady(v, budgetMs)
222
253
  if (!presented && i === 0) {
223
254
  const nudge = Math.min(step * 0.25, 0.004)
224
- await new Promise((resolve) => { v.currentTime = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001)); v.onseeked = () => resolve() })
255
+ const target = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001))
256
+ await seekOnce(v, target)
225
257
  }
226
258
 
227
259
  ctx.drawImage(v, 0, 0, canvas.width, canvas.height)
@@ -230,7 +262,11 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
230
262
  vf.close()
231
263
 
232
264
  if (typeof onProgress === 'function') {
233
- try { onProgress(Math.min(1, (i + 1) / frames)) } catch (_) {}
265
+ try {
266
+ onProgress(Math.min(1, (i + 1) / frames))
267
+ } catch (err) {
268
+ console.warn('straight-to-video: onProgress callback threw; ignoring error', err)
269
+ }
234
270
  }
235
271
  }
236
272
  await ve.flush()
@@ -254,7 +290,6 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
254
290
  const sample = new AudioSample({ format: 'f32', sampleRate: TARGET_AUDIO_SR, numberOfChannels: TARGET_AUDIO_CHANNELS, timestamp: 0, data: interleaved })
255
291
  await audioSource.add(sample)
256
292
  audioSource.close()
257
-
258
293
  await output.finalize()
259
294
  const { buffer } = output.target
260
295
  const payload = new Uint8Array(buffer)
@@ -322,6 +357,11 @@ function registerStraightToVideoController (app, opts = {}) {
322
357
  }
323
358
 
324
359
  async _processFileInput (fileInput) {
360
+ if (!this._pendingProcesses) this._pendingProcesses = new WeakMap()
361
+ const existing = this._pendingProcesses.get(fileInput)
362
+ if (existing) return existing
363
+
364
+ const job = (async () => {
325
365
  this._markFlag(fileInput, 'processing')
326
366
  fileInput.disabled = true
327
367
  try {
@@ -340,6 +380,13 @@ function registerStraightToVideoController (app, opts = {}) {
340
380
  fileInput.disabled = false
341
381
  this._unmarkFlag(fileInput, 'processing')
342
382
  }
383
+ })()
384
+
385
+ this._pendingProcesses.set(fileInput, job)
386
+ job.finally(() => {
387
+ if (this._pendingProcesses?.get(fileInput) === job) this._pendingProcesses.delete(fileInput)
388
+ })
389
+ return job
343
390
  }
344
391
 
345
392
  _fire (el, name, detail = {}) {
@@ -1,3 +1,3 @@
1
1
  module StraightToVideo
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "straight-to-video",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "straight-to-video",
9
- "version": "0.0.5",
9
+ "version": "0.0.7",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "mediabunny": "^1.24.4"
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "straight-to-video",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Browser-based, hardware-accelerated video upload optimization",
5
5
  "type": "module",
6
6
  "exports": {
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.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls