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 planNew flow
Pick video in lobby creation form → room created successfully → navigate to leader pageNavigate 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

Jung Bada
It's an app where everyone watches the choreography video they prepared, synced to the leader's controls. The plan was to pick the video in the lobby, create the room, then send the user to the leader page.
Jung Haneul
But after that navigation, would the selected 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.
Park Doeun
So because of that boundary, users might have to pick the file again. If you just write down the blob: URL made from the file, can the next page use it?
Park Dohyun
The URL alone doesn't guarantee anything. Once the document that created it is cleaned up, its link to the file is removed too. Keeping a string isn't the same as keeping a file. W3C File API
Jung Bada
Then let's go to the leader page first and pick there. If create mode and control mode both live in that page, we never have to go to another document after creating the room.
Park Doeun
How do you split those two modes in code? Just giving them two names doesn't keep the selected file around. I want to see what behavior actually changes.
Jung Haneul
Like the example above: hook up the playback URL with 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.
Park Dohyun
OK, that settles where the file gets picked. But we can't stop at "room creation passed." We need to check that it carries through from fingerprinting the chosen video all the way to playback.
Park Doeun
What's this fingerprint? Does it compare file names, or read inside the video? Even if each participant picks their own file, we need to know it's the same video.
Jung Haneul
It's a hash computed by reading part of the video, an identifier for comparing content. The viewer also picks their own file, checks that value, and then joins.
Park Doeun
The value matched and the video was ready. But the test viewer's playback position was stuck at 0 seconds. Confirming the file can be read isn't the end of it.
Jung Bada
Isn't the stall because the test video is too short? It's 1.39 seconds, so it may have ended before the viewer joined. Let's put the leader on loop and look.
Jung Haneul
It still looks stuck even on loop. I listened for the sync signals (playback state and position) on a separate connection: the signals arrive, but only the viewer's screen doesn't reflect them.
Jung Bada
Then the rendering code is the culprit, right? The connection works and it still stalled, so that sounds plausible. Let's first check whether a clean reload does the same thing.
Park Doeun
After the reload it follows along: 0.40, 0.82, 1.23 seconds. That's not from catching and fixing a handler exception. It followed once the test state started fresh.
Jung Bada
Then I should drop the idea that it's the rendering code. At the time we put it down to joining after playback had ended, plus state manipulation during the test. You can't pick a culprit just from seeing a stall.
Park Dohyun
Right. And this stall can't be used as evidence that the file got lost in the lobby either. It was a separate stall that happened after file readiness and the fingerprint match had already been confirmed.
Park Doeun
One more thing. The file the leader picks on the leader page doesn't get passed to participants automatically. Participants pick their own file on the viewer page and check the fingerprint.
Jung Haneul
So let's sum up. You don't pass the file along. You pick it on the page that uses it. And a room being created doesn't mean the video is ready, so check both.
Park Dohyun
If you want it to survive navigation, you need to design separate storage like IndexedDB. We took the cheaper option and pick the file once, on the page that uses it.