openvibers/openvibe.mediahub Commits
The Clip menu starts where the video is (start at the current second, end 30 s on), and says an empty title is named by AI (#145)
Changes
@@@ -334,7 +334,26 @@transcript(video);if (!video) return;-- // "Make a clip": with the player here, mark the start and end at the current time.++ // "Make a clip": opening the Clip menu starts it where the video is (owner, 2026-10-27: "it should have the++ // start/end time set based on the current timestamp the user is on rather than being at 0"): the start at the++ // current second, the end 30 s on, both inside the video. Opened again later, it follows the video again, unless++ // the person typed their own times.++ const clipper = document.querySelector('details.ovv-clipper');++ const startIn = document.querySelector('[data-ovv-clip-start]');++ const endIn = document.querySelector('[data-ovv-clip-end]');++ if (clipper && startIn && endIn) {++ let typed = false;++ [startIn, endIn].forEach(function (i) { i.addEventListener('input', function () { typed = true; }); });++ clipper.addEventListener('toggle', function () {++ if (!clipper.open || typed) return;++ const total = Number(startIn.getAttribute('data-ovv-clip-duration')) || video.duration || 0;++ let a = Math.max(0, Math.floor(video.currentTime || 0));++ let b = a + 30;++ if (total > 1 && b > total) { b = Math.floor(total); a = Math.max(0, b - 30); }++ startIn.value = clockOf(a);++ endIn.value = clockOf(b);++ });++ }const now = document.querySelector('[data-ovv-clip-now]');if (now) {now.hidden = false;@@@ -342,7 +361,7 @@b.addEventListener('click', function () {const which = b.getAttribute('data-ovv-clip-mark');const input = document.querySelector(which === 'start' ? '[data-ovv-clip-start]' : '[data-ovv-clip-end]');-- if (input) input.value = clockOf(video.currentTime);++ if (input) { input.value = clockOf(video.currentTime); input.dispatchEvent(new Event('input')); }if (which === 'start') {const end = document.querySelector('[data-ovv-clip-end]');if (end && !end.value) end.value = clockOf(video.currentTime + 30);@@@ -601,11 +601,12 @@ function createVideoRoutes(ctx) {return actionMenu({ icon: 'fa-solid fa-scissors', label: 'Clip', title: `Make a clip of up to ${CLIP_MAX_SECONDS} seconds`, cls: 'ovv-clipper', body: html`<p class="ovv-pop-title">Make a clip</p><form class="ovv-owner-form" method="post" action="/v/${id}/clip"><div class="ovv-clip-times">--<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start></label>++<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start${dur ? html` data-ovv-clip-duration="${dur}"` : ''}></label><label class="ovv-field"><span>End</span><input name="end" type="text" inputmode="numeric" placeholder="0:30" pattern="[0-9:.]{1,12}" required data-ovv-clip-end></label></div><p class="ovv-clip-now" hidden data-ovv-clip-now><button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="start">Start here</button> <button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="end">End here</button></p>--<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100"></label>++<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100" placeholder="Leave empty: AI names it" aria-describedby="ovv-clip-title-note"></label>++<p class="ovv-fine" id="ovv-clip-title-note">With no title, AI names the clip from what happens and what is said in it, a minute or so after it is cut.</p><p class="ovv-fine">Up to ${CLIP_MAX_SECONDS} seconds${dur ? `, within ${clock(dur)}` : ''}. Times are m:ss or h:mm:ss. It is cut on OpenVibe.Media and credited to you.</p><button class="ovv-btn ovv-btn--primary" type="submit">Make the clip</button></form>` });@@@ -74,6 +74,12 @@ function vreq(base, path, { method = 'GET', cookie, form, same = true } = {}) {assert.ok(r.text.includes('action="/v/101/clip"'), 'any signed-in viewer, not only the channel');assert.ok(r.text.includes('within 1:00:00'), 'the recording length');assert.ok(r.text.includes('data-ovv-clip-now'), 'the Start here / End here buttons (shown by the script)');++ // Owner, 2026-10-27: the times start where the video is, and an empty title is named by AI (said on the form).++ assert.ok(r.text.includes('data-ovv-clip-duration="3600"'), 'the script keeps the times inside the video');++ assert.ok(r.text.includes('placeholder="Leave empty: AI names it"') && r.text.includes('With no title, AI names the clip from what happens and what is said in it'));++ const js = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'js', 'video.js'), 'utf8');++ assert.ok(js.includes("clipper.addEventListener('toggle'") && js.includes('Math.floor(video.currentTime || 0)'), 'opening the menu starts at the current time');++ assert.ok(js.includes('if (!clipper.open || typed) return;'), 'times the person typed are kept');});await check('a clip is made on Live as the person, with their address, then its page says it is being cut', async () => {
@@ -334,7 +334,26 @@
@@ -334,7 +334,26 @@
transcript(video);
transcript(video);
if (!video) return;
if (!video) return;
- // "Make a clip": with the player here, mark the start and end at the current time.
+ // "Make a clip": opening the Clip menu starts it where the video is (owner, 2026-10-27: "it should have the
+ // start/end time set based on the current timestamp the user is on rather than being at 0"): the start at the
+ // current second, the end 30 s on, both inside the video. Opened again later, it follows the video again, unless
+ // the person typed their own times.
+ const clipper = document.querySelector('details.ovv-clipper');
+ const startIn = document.querySelector('[data-ovv-clip-start]');
+ const endIn = document.querySelector('[data-ovv-clip-end]');
+ if (clipper && startIn && endIn) {
+ let typed = false;
+ [startIn, endIn].forEach(function (i) { i.addEventListener('input', function () { typed = true; }); });
+ clipper.addEventListener('toggle', function () {
+ if (!clipper.open || typed) return;
+ const total = Number(startIn.getAttribute('data-ovv-clip-duration')) || video.duration || 0;
+ let a = Math.max(0, Math.floor(video.currentTime || 0));
+ let b = a + 30;
+ if (total > 1 && b > total) { b = Math.floor(total); a = Math.max(0, b - 30); }
+ startIn.value = clockOf(a);
+ endIn.value = clockOf(b);
+ });
+ }
const now = document.querySelector('[data-ovv-clip-now]');
const now = document.querySelector('[data-ovv-clip-now]');
if (now) {
if (now) {
now.hidden = false;
now.hidden = false;
@@ -342,7 +361,7 @@
@@ -342,7 +361,7 @@
b.addEventListener('click', function () {
b.addEventListener('click', function () {
const which = b.getAttribute('data-ovv-clip-mark');
const which = b.getAttribute('data-ovv-clip-mark');
const input = document.querySelector(which === 'start' ? '[data-ovv-clip-start]' : '[data-ovv-clip-end]');
const input = document.querySelector(which === 'start' ? '[data-ovv-clip-start]' : '[data-ovv-clip-end]');
- if (input) input.value = clockOf(video.currentTime);
+ if (input) { input.value = clockOf(video.currentTime); input.dispatchEvent(new Event('input')); }
if (which === 'start') {
if (which === 'start') {
const end = document.querySelector('[data-ovv-clip-end]');
const end = document.querySelector('[data-ovv-clip-end]');
if (end && !end.value) end.value = clockOf(video.currentTime + 30);
if (end && !end.value) end.value = clockOf(video.currentTime + 30);
@@ -601,11 +601,12 @@ function createVideoRoutes(ctx) {
@@ -601,11 +601,12 @@ function createVideoRoutes(ctx) {
return actionMenu({ icon: 'fa-solid fa-scissors', label: 'Clip', title: `Make a clip of up to ${CLIP_MAX_SECONDS} seconds`, cls: 'ovv-clipper', body: html`<p class="ovv-pop-title">Make a clip</p>
return actionMenu({ icon: 'fa-solid fa-scissors', label: 'Clip', title: `Make a clip of up to ${CLIP_MAX_SECONDS} seconds`, cls: 'ovv-clipper', body: html`<p class="ovv-pop-title">Make a clip</p>
<form class="ovv-owner-form" method="post" action="/v/${id}/clip">
<form class="ovv-owner-form" method="post" action="/v/${id}/clip">
<div class="ovv-clip-times">
<div class="ovv-clip-times">
-<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start></label>
+<label class="ovv-field"><span>Start</span><input name="start" type="text" inputmode="numeric" placeholder="0:00" pattern="[0-9:.]{1,12}" required data-ovv-clip-start${dur ? html` data-ovv-clip-duration="${dur}"` : ''}></label>
<label class="ovv-field"><span>End</span><input name="end" type="text" inputmode="numeric" placeholder="0:30" pattern="[0-9:.]{1,12}" required data-ovv-clip-end></label>
<label class="ovv-field"><span>End</span><input name="end" type="text" inputmode="numeric" placeholder="0:30" pattern="[0-9:.]{1,12}" required data-ovv-clip-end></label>
</div>
</div>
<p class="ovv-clip-now" hidden data-ovv-clip-now><button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="start">Start here</button> <button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="end">End here</button></p>
<p class="ovv-clip-now" hidden data-ovv-clip-now><button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="start">Start here</button> <button type="button" class="ovv-btn ovv-btn--small" data-ovv-clip-mark="end">End here</button></p>
-<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100"></label>
+<label class="ovv-field"><span>Title <span class="ovv-fine">(optional)</span></span><input name="title" type="text" maxlength="100" placeholder="Leave empty: AI names it" aria-describedby="ovv-clip-title-note"></label>
+<p class="ovv-fine" id="ovv-clip-title-note">With no title, AI names the clip from what happens and what is said in it, a minute or so after it is cut.</p>
<p class="ovv-fine">Up to ${CLIP_MAX_SECONDS} seconds${dur ? `, within ${clock(dur)}` : ''}. Times are m:ss or h:mm:ss. It is cut on OpenVibe.Media and credited to you.</p>
<p class="ovv-fine">Up to ${CLIP_MAX_SECONDS} seconds${dur ? `, within ${clock(dur)}` : ''}. Times are m:ss or h:mm:ss. It is cut on OpenVibe.Media and credited to you.</p>
<button class="ovv-btn ovv-btn--primary" type="submit">Make the clip</button>
<button class="ovv-btn ovv-btn--primary" type="submit">Make the clip</button>
</form>` });
</form>` });
@@ -74,6 +74,12 @@ function vreq(base, path, { method = 'GET', cookie, form, same = true } = {}) {
@@ -74,6 +74,12 @@ function vreq(base, path, { method = 'GET', cookie, form, same = true } = {}) {
assert.ok(r.text.includes('action="/v/101/clip"'), 'any signed-in viewer, not only the channel');
assert.ok(r.text.includes('action="/v/101/clip"'), 'any signed-in viewer, not only the channel');
assert.ok(r.text.includes('within 1:00:00'), 'the recording length');
assert.ok(r.text.includes('within 1:00:00'), 'the recording length');
assert.ok(r.text.includes('data-ovv-clip-now'), 'the Start here / End here buttons (shown by the script)');
assert.ok(r.text.includes('data-ovv-clip-now'), 'the Start here / End here buttons (shown by the script)');
+ // Owner, 2026-10-27: the times start where the video is, and an empty title is named by AI (said on the form).
+ assert.ok(r.text.includes('data-ovv-clip-duration="3600"'), 'the script keeps the times inside the video');
+ assert.ok(r.text.includes('placeholder="Leave empty: AI names it"') && r.text.includes('With no title, AI names the clip from what happens and what is said in it'));
+ const js = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'js', 'video.js'), 'utf8');
+ assert.ok(js.includes("clipper.addEventListener('toggle'") && js.includes('Math.floor(video.currentTime || 0)'), 'opening the menu starts at the current time');
+ assert.ok(js.includes('if (!clipper.open || typed) return;'), 'times the person typed are kept');
});
});
await check('a clip is made on Live as the person, with their address, then its page says it is being cut', async () => {
await check('a clip is made on Live as the person, with their address, then its page says it is being cut', async () => {