Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 69 additions & 8 deletions preview/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -663,17 +663,17 @@ <h2 class="canvas-title" id="canvas-title">Example podcast layout canvas</h2>
<div class="drop-zone host" data-slot="host" tabindex="0" role="button" aria-label="Place the selected track into the host video slot">
<span class="slot-label">Host video slot</span>
<span class="slot-note">Drop the host video file here, or choose one. Keep room for captions and chapter callouts.</span>
<input class="slot-file" type="file" accept="video/*" data-file-input="host" aria-label="Choose host video file">
<input class="slot-file" type="file" accept="video/*" multiple data-file-input="host" aria-label="Choose host video file">
</div>
<div class="drop-zone guest" data-slot="guest" tabindex="0" role="button" aria-label="Place the selected track into the guest video slot">
<span class="slot-label">Guest video slot</span>
<span class="slot-note">Drop the guest video file here, or choose one, matched to framing and safe-area rules.</span>
<input class="slot-file" type="file" accept="video/*" data-file-input="guest" aria-label="Choose guest video file">
<input class="slot-file" type="file" accept="video/*" multiple data-file-input="guest" aria-label="Choose guest video file">
</div>
<div class="drop-zone guest-b is-hidden" data-slot="guest-b" tabindex="0" role="button" aria-label="Place the selected track into the second guest video slot">
<span class="slot-label">Guest 2 video slot</span>
<span class="slot-note">Drop the second guest video file here, or choose one, when the panel grid is selected.</span>
<input class="slot-file" type="file" accept="video/*" data-file-input="guest-b" aria-label="Choose second guest video file">
<input class="slot-file" type="file" accept="video/*" multiple data-file-input="guest-b" aria-label="Choose second guest video file">
</div>
</div>
<div class="canvas-footer">
Expand All @@ -684,7 +684,7 @@ <h2 class="canvas-title" id="canvas-title">Example podcast layout canvas</h2>
<div class="title-slot drop-zone broll" data-slot="broll" tabindex="0" role="button" aria-label="Place the selected track into the b-roll drop zone">
<span class="slot-label">B-roll drop zone</span>
<div class="slot-note">Place contextual visuals, title cards, or screen shares without hiding the conversation.</div>
<input class="slot-file" type="file" accept="video/*" data-file-input="broll" aria-label="Choose optional b-roll video file">
<input class="slot-file" type="file" accept="video/*" multiple data-file-input="broll" aria-label="Choose optional b-roll video file">
</div>
</div>
</div>
Expand Down Expand Up @@ -1498,6 +1498,67 @@ <h3 class="group-title">Review &amp; publish</h3>
updateSlotStatus();
}

// A file the placement path will actually accept into a slot: a video that carries real
// bytes. A non-video or a 0-byte export passes neither, so it never consumes a slot.
function isPlaceableVideo(file) {
return isVideoFile(file) && !isEmptyExport(file);
}

// The visible slots, in layout order, that are still empty — where extra recordings from a
// multi-file drop spill (required speaker slots first, optional b-roll last). Optionally
// excludes the slot the drop landed on, which already took the first file.
function openVisibleSlots(exceptZone) {
return currentDefinition().visibleSlots
.map(function (slot) { return document.querySelector(slotSelector(slot)); })
.filter(function (zone) {
return zone && zone !== exceptZone && !zone.classList.contains("filled");
});
}

// Place several recordings from one drop or file pick: fill the slot they landed on, then
// spill the remaining videos into the next empty visible slots in order. This lets a
// creator drop all of their synced speaker recordings onto the layout at once instead of
// one slot at a time, matching the layout-first start. Duplicate copies of the same source
// in a single drop are dropped (one source can't fill two speaker slots), and the creator
// is told when videos overflow the open slots or a non-video file was skipped.
function placeVideoFiles(zone, fileList) {
var all = Array.prototype.slice.call(fileList || []).filter(Boolean);
var seen = {};
var files = all.filter(function (file) {
var sig = fileSignature(file);
if (!sig) return true;
if (seen[sig]) return false;
seen[sig] = true;
return true;
});
if (!files.length) return;
placeVideoFile(zone, files[0]);
if (!zone.classList.contains("filled")) {
// The target slot rejected the first file — do not spill the rest into other slots.
return;
}
var extras = files.slice(1).filter(isPlaceableVideo);
var skippedNonVideo = files.slice(1).length - extras.length;
var open = openVisibleSlots(zone);
var overflow = 0;
extras.forEach(function (file, index) {
if (open[index]) {
placeVideoFile(open[index], file);
} else {
overflow += 1;
}
});
if (overflow > 0) {
updateSlotStatus("There's no open slot left, so " + overflow + " extra "
+ (overflow === 1 ? "video wasn't" : "videos weren't")
+ " placed. Remove a video to make room for another.");
} else if (skippedNonVideo > 0) {
updateSlotStatus(skippedNonVideo + " "
+ (skippedNonVideo === 1 ? "file in that drop wasn't a video, so it was" : "files in that drop weren't videos, so they were")
+ " skipped. Only video files can fill a slot.");
}
}

function placeTrack(zone, track) {
if (!zone || track !== zone.dataset.slot) {
updateSlotStatus("That track belongs in a different slot.");
Expand Down Expand Up @@ -1602,9 +1663,9 @@ <h3 class="group-title">Review &amp; publish</h3>
zone.addEventListener("drop", function (event) {
event.preventDefault();
zone.classList.remove("drag-over");
var file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (file) {
placeVideoFile(zone, file);
var files = event.dataTransfer && event.dataTransfer.files;
if (files && files.length) {
placeVideoFiles(zone, files);
return;
}
var track = event.dataTransfer.getData("text/plain");
Expand Down Expand Up @@ -1660,7 +1721,7 @@ <h3 class="group-title">Review &amp; publish</h3>
document.querySelectorAll(".slot-file").forEach(function (input) {
input.addEventListener("change", function () {
var zone = document.querySelector('.drop-zone[data-slot="' + input.dataset.fileInput + '"]');
placeVideoFile(zone, input.files && input.files[0]);
placeVideoFiles(zone, input.files);
});
});

Expand Down
37 changes: 37 additions & 0 deletions preview/layout-first-canvas-handoff.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,41 @@ assert.equal(panelButton.getAttribute("aria-checked"), "true", "End applies the
pressKey(panelButton, "Home");
assert.equal(interviewButton.getAttribute("aria-checked"), "true", "Home applies the first layout (interview)");

// Multi-file placement: dropping several recordings at once fills the slot they land on and
// spills the rest into the next empty visible slots, like the layout-first start.
function vid(name, overrides) {
return Object.assign({ name: name, type: "video/mp4", size: 2048, lastModified: 1 }, overrides || {});
}
function dropFiles(slot, files) {
const zone = zones.find((entry) => entry.dataset.slot === slot);
zone.listeners.drop({ preventDefault() {}, dataTransfer: { files: files, getData() { return ""; } } });
}
function filled(slot) {
return zones.find((zone) => zone.dataset.slot === slot).classList.contains("filled");
}

resetButton.click();
dropFiles("host", [vid("host.mp4", { lastModified: 1 }), vid("guest.mp4", { lastModified: 2 })]);
assert.equal(filled("host"), true, "a multi-file drop fills the slot it landed on");
assert.equal(filled("guest"), true, "the extra recording spills into the next empty slot");

// A non-video in the batch is skipped (the videos still place) and the creator is told.
resetButton.click();
dropFiles("host", [vid("host.mp4", { lastModified: 1 }), { name: "poster.txt", type: "text/plain", size: 10 }, vid("guest.mp4", { lastModified: 2 })]);
assert.equal(filled("host"), true, "the first video fills the target slot even with a non-video in the batch");
assert.equal(filled("guest"), true, "the other video still spills past the skipped non-video");
assert.match(slotStatus.textContent, /wasn't a video, so it was skipped/, "the skipped non-video is reported");

// Overflow: more videos than open slots (host, guest, broll = 3 in interview) reports the surplus.
resetButton.click();
dropFiles("host", [vid("a.mp4", { lastModified: 1 }), vid("b.mp4", { lastModified: 2 }), vid("c.mp4", { lastModified: 3 }), vid("d.mp4", { lastModified: 4 })]);
assert.match(slotStatus.textContent, /1 extra video wasn't placed/, "an overflowing multi-file drop reports the surplus");
assert.equal(filled("broll"), true, "the drop fills every available slot before overflowing");

// De-duplicate: the same recording twice fills only the slot it landed on.
resetButton.click();
dropFiles("host", [vid("dupe.mp4", { size: 5, lastModified: 9 }), vid("dupe.mp4", { size: 5, lastModified: 9 })]);
assert.equal(filled("host"), true, "a duplicate batch keeps the recording in the slot it landed on");
assert.equal(filled("guest"), false, "a duplicate of the same recording does not spill into another slot");

console.log("layout-first canvas handoff: keyboard, click, and drag placement all unlock continue correctly");
6 changes: 3 additions & 3 deletions preview/preview-canvas-video.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf8");
const script = html.match(/<script>\s*\(function \(\) \{([\s\S]*?)\}\(\)\);\s*<\/script>/)[1];

// The slots also expose a real video file input wired into placement.
assert.match(html, /class="slot-file" type="file" accept="video\/\*" data-file-input="host"/, "host slot accepts a real video file");
assert.match(html, /class="slot-file" type="file" accept="video\/\*" data-file-input="guest"/, "guest slot accepts a real video file");
assert.match(html, /class="slot-file" type="file" accept="video\/\*" data-file-input="broll"/, "b-roll slot accepts a real video file");
assert.match(html, /class="slot-file" type="file" accept="video\/\*" multiple data-file-input="host"/, "host slot accepts a real video file");
assert.match(html, /class="slot-file" type="file" accept="video\/\*" multiple data-file-input="guest"/, "guest slot accepts a real video file");
assert.match(html, /class="slot-file" type="file" accept="video\/\*" multiple data-file-input="broll"/, "b-roll slot accepts a real video file");

let nodeId = 0;

Expand Down
4 changes: 2 additions & 2 deletions preview/preview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ assert.ok(
);
assert.match(
html,
/class="slot-file" type="file" accept="video\/\*" data-file-input="host"/,
/class="slot-file" type="file" accept="video\/\*" multiple data-file-input="host"/,
"preview canvas accepts a real uploaded host video, not only the sample track",
);
assert.match(
html,
/class="slot-file" type="file" accept="video\/\*" data-file-input="guest-b"/,
/class="slot-file" type="file" accept="video\/\*" multiple data-file-input="guest-b"/,
"preview canvas accepts a real uploaded second guest video when the panel layout is selected",
);
assert.match(
Expand Down