hlassiege/darylpublic Fork
⑂ b963e6a
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

fix(web): release preview media elements when they leave the page

Removed <video>/<audio> elements kept downloading until garbage collected.
Over HTTP/2 nothing caps these requests, and each kept a proxied connection
to the API open; scrubbing across B-roll piled them up past the 512 sockets
select() handles on Windows, crashing the API.
hlassiege committed 2026-10-12T09:50:36+02:00 Browse files
b963e6a parent: 42bca16
added web/src/lib/media.ts +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+/**
2+ * Lâche le fichier d'un <video> / <audio> qu'on n'utilise plus. Sans ça, l'élément retiré de la page continue
3+ * de télécharger jusqu'à son ramasse-miettes : en HTTP/2 (Vite en HTTPS) rien ne plafonne ces requêtes, et
4+ * chacune garde une connexion ouverte vers l'API, qui tombe au-delà de 512 (select() de Windows).
5+ */
6+export function releaseMedia(m: HTMLMediaElement | null | undefined) {
7+ if (!m) return;
8+ m.pause();
9+ m.removeAttribute("src");
10+ // sans src, load() coupe la requête en cours
11+ m.load();
12+}
new file mode 100644
@@ -0,0 +1,12 @@
1+/**
2+ * Lâche le fichier d'un <video> / <audio> qu'on n'utilise plus. Sans ça, l'élément retiré de la page continue
3+ * de télécharger jusqu'à son ramasse-miettes : en HTTP/2 (Vite en HTTPS) rien ne plafonne ces requêtes, et
4+ * chacune garde une connexion ouverte vers l'API, qui tombe au-delà de 512 (select() de Windows).
5+ */
6+export function releaseMedia(m: HTMLMediaElement | null | undefined) {
7+ if (!m) return;
8+ m.pause();
9+ m.removeAttribute("src");
10+ // sans src, load() coupe la requête en cours
11+ m.load();
12+}
modified web/src/project/steps/montage/LayerVideo.vue +3 -1
@@ -1,6 +1,7 @@
11 <script setup lang="ts">
22 /** Une vidéo muette de l'aperçu (un B-roll), calée sur `time` (secondes dans le média) et sur la lecture. */
3-import { onMounted, ref, watch } from "vue";
3+import { onBeforeUnmount, onMounted, ref, watch } from "vue";
4+import { releaseMedia } from "../../../lib/media";
45
56 const props = defineProps<{ src: string; time: number; playing: boolean }>();
67 const el = ref<HTMLVideoElement | null>(null);
@@ -15,6 +16,7 @@ function sync() {
1516 }
1617 watch(() => [props.time, props.playing, props.src], sync);
1718 onMounted(sync);
19+onBeforeUnmount(() => releaseMedia(el.value));
1820 </script>
1921
2022 <template>
@@ -1,6 +1,7 @@
1 <script setup lang="ts">1 <script setup lang="ts">
2 /** Une vidéo muette de l'aperçu (un B-roll), calée sur `time` (secondes dans le média) et sur la lecture. */2 /** Une vidéo muette de l'aperçu (un B-roll), calée sur `time` (secondes dans le média) et sur la lecture. */
3-import { onMounted, ref, watch } from "vue";3+import { onBeforeUnmount, onMounted, ref, watch } from "vue";
4+import { releaseMedia } from "../../../lib/media";
4 5
5 const props = defineProps<{ src: string; time: number; playing: boolean }>();6 const props = defineProps<{ src: string; time: number; playing: boolean }>();
6 const el = ref<HTMLVideoElement | null>(null);7 const el = ref<HTMLVideoElement | null>(null);
@@ -15,6 +16,7 @@ function sync() {
15 }16 }
16 watch(() => [props.time, props.playing, props.src], sync);17 watch(() => [props.time, props.playing, props.src], sync);
17 onMounted(sync);18 onMounted(sync);
19+onBeforeUnmount(() => releaseMedia(el.value));
18 </script>20 </script>
19 21
20 <template>22 <template>
modified web/src/project/steps/montage/Preview.vue +5 -4
@@ -16,6 +16,7 @@ import type { Playlist } from "../derush/usePlaylist";
1616 import SlideFrame from "../shorts/SlideFrame.vue";
1717 import { fxFrame, layersAt } from "../../../lib/transitions";
1818 import LayerVideo from "./LayerVideo.vue";
19+import { releaseMedia } from "../../../lib/media";
1920 import {
2021 clipStarts, isImage, tcd, TEXT_BORDER, TEXT_FADE, TEXT_FONT, type AnimState, type Beat, type MontageView, type TextNote, type Timeline, type Visual,
2122 } from "./types";
@@ -49,7 +50,7 @@ async function copySearch(b: Beat) {
4950
5051 const el = ref<HTMLVideoElement | null>(null);
5152 onMounted(() => { props.player.video.value = el.value; });
52-onBeforeUnmount(() => { props.player.video.value = null; });
53+onBeforeUnmount(() => { props.player.video.value = null; releaseMedia(el.value); });
5354
5455 const t = computed(() => props.player.t.value);
5556 const playing = computed(() => props.player.playing.value);
@@ -221,7 +222,7 @@ watch([t, playing], () => {
221222 let a = players.get(s.id);
222223 // a.src est absolue et encodée (espaces → %20) : on compare avec l'adresse résolue de la même façon
223224 if (!a || a.src !== new URL(url, location.href).href) {
224- a?.pause();
225+ releaseMedia(a);
225226 a = new Audio(url);
226227 players.set(s.id, a);
227228 }
@@ -231,9 +232,9 @@ watch([t, playing], () => {
231232 if (a.paused) a.play().catch(() => {});
232233 }
233234 }
234- for (const [id, a] of players) if (!active.has(id)) { a.pause(); players.delete(id); }
235+ for (const [id, a] of players) if (!active.has(id)) { releaseMedia(a); players.delete(id); }
235236 });
236-onUnmounted(() => { for (const a of players.values()) a.pause(); players.clear(); });
237+onUnmounted(() => { for (const a of players.values()) releaseMedia(a); players.clear(); });
237238
238239 // un beat posé dont l'animation n'est pas écrite reste une slide, qu'on peut relancer ; un zoom ou un B-roll
239240 // vidéo se voit déjà, une étiquette dit qu'il reste à poser
@@ -16,6 +16,7 @@ import type { Playlist } from "../derush/usePlaylist";
16 import SlideFrame from "../shorts/SlideFrame.vue";16 import SlideFrame from "../shorts/SlideFrame.vue";
17 import { fxFrame, layersAt } from "../../../lib/transitions";17 import { fxFrame, layersAt } from "../../../lib/transitions";
18 import LayerVideo from "./LayerVideo.vue";18 import LayerVideo from "./LayerVideo.vue";
19+import { releaseMedia } from "../../../lib/media";
19 import {20 import {
20 clipStarts, isImage, tcd, TEXT_BORDER, TEXT_FADE, TEXT_FONT, type AnimState, type Beat, type MontageView, type TextNote, type Timeline, type Visual,21 clipStarts, isImage, tcd, TEXT_BORDER, TEXT_FADE, TEXT_FONT, type AnimState, type Beat, type MontageView, type TextNote, type Timeline, type Visual,
21 } from "./types";22 } from "./types";
@@ -49,7 +50,7 @@ async function copySearch(b: Beat) {
49 50
50 const el = ref<HTMLVideoElement | null>(null);51 const el = ref<HTMLVideoElement | null>(null);
51 onMounted(() => { props.player.video.value = el.value; });52 onMounted(() => { props.player.video.value = el.value; });
52-onBeforeUnmount(() => { props.player.video.value = null; });53+onBeforeUnmount(() => { props.player.video.value = null; releaseMedia(el.value); });
53 54
54 const t = computed(() => props.player.t.value);55 const t = computed(() => props.player.t.value);
55 const playing = computed(() => props.player.playing.value);56 const playing = computed(() => props.player.playing.value);
@@ -221,7 +222,7 @@ watch([t, playing], () => {
221 let a = players.get(s.id);222 let a = players.get(s.id);
222 // a.src est absolue et encodée (espaces → %20) : on compare avec l'adresse résolue de la même façon223 // a.src est absolue et encodée (espaces → %20) : on compare avec l'adresse résolue de la même façon
223 if (!a || a.src !== new URL(url, location.href).href) {224 if (!a || a.src !== new URL(url, location.href).href) {
224- a?.pause();225+ releaseMedia(a);
225 a = new Audio(url);226 a = new Audio(url);
226 players.set(s.id, a);227 players.set(s.id, a);
227 }228 }
@@ -231,9 +232,9 @@ watch([t, playing], () => {
231 if (a.paused) a.play().catch(() => {});232 if (a.paused) a.play().catch(() => {});
232 }233 }
233 }234 }
234- for (const [id, a] of players) if (!active.has(id)) { a.pause(); players.delete(id); }235+ for (const [id, a] of players) if (!active.has(id)) { releaseMedia(a); players.delete(id); }
235 });236 });
236-onUnmounted(() => { for (const a of players.values()) a.pause(); players.clear(); });237+onUnmounted(() => { for (const a of players.values()) releaseMedia(a); players.clear(); });
237 238
238 // un beat posé dont l'animation n'est pas écrite reste une slide, qu'on peut relancer ; un zoom ou un B-roll239 // un beat posé dont l'animation n'est pas écrite reste une slide, qu'on peut relancer ; un zoom ou un B-roll
239 // vidéo se voit déjà, une étiquette dit qu'il reste à poser240 // vidéo se voit déjà, une étiquette dit qu'il reste à poser