Dev Investigation Notes
If users have to re-pick a local video on the next page
This is a translated article. Read the original in Korean ↗
I was building a web app for dance practice (formation). Each person plays a choreography video they prepared themselves, and playback follows the leader's controls. While implementing and testing the cloud version on a local dev server in the browser, a constraint showed up in the planned flow: pick a video in the lobby, then move to the leader page. Nobody had confirmed that the chosen video File would still be usable on the page you land on, so the room-creation form was moved to the page that actually uses the video. The file going missing was never reproduced. This is a case of changing the plan mid-implementation.
Conclusion
Q. If the user picks a file and then navigates to another page, do they have to pick it again? A. Don't assume the next page can use that file. The simplest approach is to have the user pick it on the page that plays it. Neither a blob: URL string nor a File carries over to the next document automatically. Keeping it across navigation takes a separate design, such as storing it in IndexedDB (see the Cause section below).
The fix here doesn't hand the file to the next document. The user picks it once on the page that uses it, and only the view changes within that document. The file picker moved from the lobby to the leader page.
A blob: URL is not a permanent address for a file. When the document that created it goes through unload cleanup, the mapping is removed, so don't expect to save just the string and reuse it on a new page. W3C File API §8.3.3
Now the user goes to the leader page, where the file is used, before picking the video. leader.html has a create mode (create-mode) and a control mode (control-mode), which chain together file selection → room creation → playback control. The lobby kept the room list and the 새 연습방 만들기 (Create new practice room) button.
| Original plan | New flow |
|---|---|
| Pick video in lobby creation form → room created successfully → navigate to leader page | Navigate from lobby to leader page → pick name and video → create room → control mode on the same page |
This flow is backed by the implementation plan at the time, the change report, and the manual test instructions given afterward. No log reproducing file loss before the change was presented, so this post covers a design change driven by a constraint found during implementation.
Situation
The goal was to get rid of a chore: before every practice, someone had to start a server on a laptop and everyone had to connect over the same Wi-Fi. The new direction was for people to share videos with each other ahead of time, and for the app to relay sync signals over a publicly reachable endpoint. Participants pick a room from the room list, choose the video file on their own device, and follow the leader's playback controls.
Three screens matter for this problem. The lobby is where you find rooms. The leader page is where you create a room and control playback. The viewer page is where a participant picks their own video and joins. After the server-side signal relay was verified, the creation form moved while these screens were being wired together.
Symptom
At first, the lobby held both the creation form and the room list, and the next step in the plan was "room created successfully → go to leader page." While writing the leader page, though, the creation feature was moved into it because "navigating from the lobby means the video File can't be kept." So this didn't start with a user reporting a lost file. It started with revising a plan that put file selection and playback on different pages.
Cause
The File you get from the lobby's file input is an object representing the selected file. The plan at the time had a step that moved to a separate HTML document after room creation, but no procedure for handing that object to the new document can be found. So the creation form moved into the leader document, which is where the file gets used. Getting a room code and having the video ready in the next document were two different conditions.
If you saved a blob URL and the preview doesn't work on the new page, check the lifetime of the document that created the URL. URL.createObjectURL(file) creates a mapping between the file and the URL in the browser's URL store. Once that mapping is removed during the creating document's unload cleanup, the saved string can no longer find the file. Saving the URL doesn't save the file data. W3C File API §8.2–8.4
That doesn't mean "a blob URL can never be used on another page." There's a difference between a case where the creating environment is still alive and the access conditions are met, and a case where the creating document has been cleaned up. Switching modes by hiding and showing elements within the same document is also different from navigating to a new HTML document. W3C File API §8.3
If you do need the file to survive navigation, there's another route: store the File (a Blob) in IndexedDB, the browser's database, then read it back on the next page and call createObjectURL again. Only pages on the same origin can read it, and the stored file takes up browser storage quota, so you have to delete it yourself. The minimal flow is below. It's a generic example that this app didn't use and that wasn't tested. Indexed Database API
// Page where the file was picked: save
const db = await new Promise((ok, err) => {
const r = indexedDB.open('picked', 1);
r.onupgradeneeded = () => r.result.createObjectStore('files');
r.onsuccess = () => ok(r.result);
r.onerror = () => err(r.error);
});
db.transaction('files', 'readwrite').objectStore('files').put(file, 'video');
// Next page: open db the same way, get the Blob with get('video'), then
// put URL.createObjectURL(blob) into video.src
Instead, this app has users pick the file on the leader page. There's no evidence that the two alternatives were tested against each other and one ruled out.
Fix
The lobby's creation form moved to leader.html, which is now split into a create mode and a control mode. The later test instructions follow the same order: on the leader page, pick a room name and a real video, create the room, then use play, pause, and seek.
Below is a condensed example of that flow. It is not a reconstruction of the actual project code. createRoom has to be wired to an app-specific function that computes the fingerprint and duration from the name and file and completes the room-creation request. Per the plan, the fingerprint is a hash of file.size plus 8MB chunks taken from the start, middle, and end with file.slice(). Hashing the whole file was avoided because of memory limits on mobile. The actual implementation code wasn't confirmed, so it isn't reconstructed here. The example only shows input change → set local playback URL → switch the view after the room is created. The evidence for the real change is the report of the two-mode structure and the local playback verification.
<section id="create-mode">
<input id="room-name" placeholder="방 이름">
<input id="video-file" type="file">
<button id="create-room" type="button">방 만들기</button>
</section>
<section id="control-mode" hidden>
<video id="video" controls></video>
</section>
<p id="message"></p>
<script>
// Call this with the app's room-creation function: bindRoomForm(createRoom)
function bindRoomForm(createRoom) {
const get = id => document.getElementById(id);
let file, objectURL;
get('video-file').onchange = event => {
file = event.target.files[0];
get('video').removeAttribute('src');
get('video').load();
if (objectURL) URL.revokeObjectURL(objectURL);
objectURL = undefined;
if (!file) return;
objectURL = URL.createObjectURL(file);
get('video').src = objectURL;
};
get('create-room').onclick = async () => {
const name = get('room-name').value.trim();
if (!name || !file) return;
get('create-room').disabled = true;
get('video-file').disabled = true;
try {
await createRoom({ name, file }); // App-specific; not a video upload
get('create-mode').hidden = true;
get('control-mode').hidden = false; // Kept without navigating to another document
get('message').textContent = '';
} catch {
get('message').textContent = '방 생성에 실패했습니다. 다시 시도하세요.';
} finally {
get('create-room').disabled = false;
get('video-file').disabled = false;
}
};
}
</script>
(UI strings in the example: 방 이름 = "Room name", 방 만들기 = "Create room", 방 생성에 실패했습니다. 다시 시도하세요. = "Failed to create the room. Please try again.")
When the file is replaced, the example detaches the old playback source and revokes the URL that's no longer in use. It does not revoke the current URL just because the view switched to control mode, because revoking a URL breaks the mapping between that URL and the file. W3C File API §8.4
A video fingerprint is an identifier used to check whether two people picked the same video. This app used a hash computed from parts of the video. A sync signal is a message that carries play/pause state and playback position instead of the video file itself. A participant picks their own file on the viewer page, checks the fingerprint, joins, and then follows these signals.
The verification after the change targeted the leader page's real code path. A playable test video was generated in the browser with MediaRecorder and injected into the file input. A report confirms that fingerprint and duration computation, room creation, local playback via createObjectURL (readyState=4), and sync signal delivery all worked. These are results from an automated test video. They don't mean that real file-picking tests on phones were all completed.
At one point during verification, the viewer stalled at 0 seconds. Even then, the viewer's fingerprint match, join, and local video readiness had already been confirmed, and after a clean reload the playback position advanced and followed the leader. So that observation can't be used as evidence of "the file getting lost in the lobby."
A later change added an optional choreography-path file input to the leader's creation form, and the post-deployment test instructions kept the same order: 새 연습방 만들기 (Create new practice room) → pick name and video → create room → play. That later flow is consistent with the decision to move the creation form.
Reporter's Notes — creating the room didn't bring the video along
File, the video file object, still be ready? That boundary came up while we were hooking up the leader page, so we moved the creation form. blob: URL made from the file, can the next page use it?video.src = objectURL, and after the room is created, only flip the display with hidden. That's an example to explain the two modes, not the original code.