straight_to_video 0.0.5 → 0.0.6

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: 57371915cd38c1ecac814d16a43244275216661f31b639a80022685672984e22
4
+ data.tar.gz: bb04ab7274edbb61031a270c8f650872b0b2338a7a1fc93a54948c8f3a3fcd04
5
5
  SHA512:
6
- metadata.gz: 54b446d8179dd83c5604902b741c15c07b388b0fbada671527faf07441d11d73af5c1ba6729128897b43cf2232b7f9dc8f8271e0037362a94155736a28309089
7
- data.tar.gz: bbe19415c4fa54b522fefbcf8aaaa208d72674aacf6e29db2830c06190076b45955b04a4e51ad75baa161abfe1ce1feb1a075970c439a863158b88459cfe26e1
6
+ metadata.gz: 92b3e2d474ba435ac6679c26755a9d62bb5388cd3fbb984fc6b31506287b4ced1be3a7c3621efdaf1812a9feae7f60287caa9e91e3601731c60c0a450e4d8824
7
+ data.tar.gz: 550b54b8fb004e7b6fea54282f00053fe47fd4bdefd20a5e87817c91495756adfe38c40d72888de883b0fbc049ad9615eb3f4f1783b05d01d92fc418356645cb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.6
4
+
5
+ - fix iOS Safari Stimulus controller hang by making `seeked` waits robust
6
+
3
7
  ## 0.0.5
4
8
 
5
9
  - 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.6 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,22 @@ 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 { v.load() } catch (_) {}
237
+ })
210
238
  const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight
211
239
  const ctx = canvas.getContext('2d', { alpha: false })
212
240
 
@@ -216,13 +244,15 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
216
244
  const drawTime = i === 0
217
245
  ? Math.min(Math.max(0, t + (step * 0.5)), Math.max(0.000001, durationCfr - 0.000001))
218
246
  : targetTime
219
-
220
- await new Promise((resolve) => { v.currentTime = drawTime; v.onseeked = () => resolve() })
247
+ if (i === 0) {}
248
+ await seekOnce(v, drawTime)
249
+ if (i === 0) {}
221
250
  const budgetMs = Math.min(34, Math.max(17, Math.round(step * 1000)))
222
251
  const presented = await waitForFrameReady(v, budgetMs)
223
252
  if (!presented && i === 0) {
224
253
  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() })
254
+ const target = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001))
255
+ await seekOnce(v, target)
226
256
  }
227
257
 
228
258
  ctx.drawImage(v, 0, 0, canvas.width, canvas.height)
@@ -255,7 +285,6 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
255
285
  const sample = new AudioSample({ format: 'f32', sampleRate: TARGET_AUDIO_SR, numberOfChannels: TARGET_AUDIO_CHANNELS, timestamp: 0, data: interleaved })
256
286
  await audioSource.add(sample)
257
287
  audioSource.close()
258
-
259
288
  await output.finalize()
260
289
  const { buffer } = output.target
261
290
  const payload = new Uint8Array(buffer)
@@ -323,6 +352,8 @@ function registerStraightToVideoController (app, opts = {}) {
323
352
  }
324
353
 
325
354
  async _processFileInput (fileInput) {
355
+ const ua = typeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : ''
356
+ const isIos = /iP(hone|ad|od)/.test(ua)
326
357
  this._markFlag(fileInput, 'processing')
327
358
  fileInput.disabled = true
328
359
  try {
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,8 +198,6 @@
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
203
  if (fileEl.showPicker) { try { fileEl.showPicker(); return } catch (_) {} }
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,22 @@ 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 { v.load() } catch (_) {}
236
+ })
209
237
  const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight
210
238
  const ctx = canvas.getContext('2d', { alpha: false })
211
239
 
@@ -215,13 +243,15 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
215
243
  const drawTime = i === 0
216
244
  ? Math.min(Math.max(0, t + (step * 0.5)), Math.max(0.000001, durationCfr - 0.000001))
217
245
  : targetTime
218
-
219
- await new Promise((resolve) => { v.currentTime = drawTime; v.onseeked = () => resolve() })
246
+ if (i === 0) {}
247
+ await seekOnce(v, drawTime)
248
+ if (i === 0) {}
220
249
  const budgetMs = Math.min(34, Math.max(17, Math.round(step * 1000)))
221
250
  const presented = await waitForFrameReady(v, budgetMs)
222
251
  if (!presented && i === 0) {
223
252
  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() })
253
+ const target = Math.min(drawTime + nudge, Math.max(0.000001, durationCfr - 0.000001))
254
+ await seekOnce(v, target)
225
255
  }
226
256
 
227
257
  ctx.drawImage(v, 0, 0, canvas.width, canvas.height)
@@ -254,7 +284,6 @@ async function encodeVideo ({ file, srcMeta, onProgress }) {
254
284
  const sample = new AudioSample({ format: 'f32', sampleRate: TARGET_AUDIO_SR, numberOfChannels: TARGET_AUDIO_CHANNELS, timestamp: 0, data: interleaved })
255
285
  await audioSource.add(sample)
256
286
  audioSource.close()
257
-
258
287
  await output.finalize()
259
288
  const { buffer } = output.target
260
289
  const payload = new Uint8Array(buffer)
@@ -322,6 +351,8 @@ function registerStraightToVideoController (app, opts = {}) {
322
351
  }
323
352
 
324
353
  async _processFileInput (fileInput) {
354
+ const ua = typeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : ''
355
+ const isIos = /iP(hone|ad|od)/.test(ua)
325
356
  this._markFlag(fileInput, 'processing')
326
357
  fileInput.disabled = true
327
358
  try {
@@ -1,3 +1,3 @@
1
1
  module StraightToVideo
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
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.6",
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.6",
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.6",
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.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls