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 +4 -4
- data/CHANGELOG.md +8 -0
- data/app/assets/javascripts/straight-to-video.js +55 -8
- data/index.html +21 -7
- data/index.js +54 -7
- data/lib/straight_to_video/version.rb +1 -1
- data/package-lock.json +2 -2
- data/package.json +1 -1
- 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: 7412c0ad6de37671f90aad3c474a2dfc2f846e656de524248791ec318ad598b7
|
|
4
|
+
data.tar.gz: 4032432057ff6b71831f3e509e4300f34a8f662c70b977923ec60edfce2d15ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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 {
|
|
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) {
|
|
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 {
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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 {
|
|
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 = {}) {
|
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.7",
|
|
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.7",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"mediabunny": "^1.24.4"
|
data/package.json
CHANGED