Compare commits

...

6 Commits

Author SHA1 Message Date
37cfb375b8 attempt fix subs fullscreen 2026-04-16 07:50:55 -07:00
ea3d92974b subs fix pause 2026-04-15 21:49:25 -07:00
0cb2a7739a some fonts 2026-04-15 21:46:06 -07:00
5d82e92461 ass subs working pt. 1 2026-04-15 21:06:52 -07:00
151c719901 ass subs attempt pt. 1 2026-04-15 20:37:12 -07:00
f7636f61a7 vtt subtitles 2026-04-15 19:08:31 -07:00
24 changed files with 162 additions and 13 deletions

View File

@@ -4,6 +4,9 @@
"workspaces": {
"": {
"name": "space-stream",
"dependencies": {
"jassub": "^2.4.2",
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-cloudflare": "^7.2.8",
@@ -272,6 +275,8 @@
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"abslink": ["abslink@1.1.6", "", {}, "sha512-8fQgnUoVSgc1IhOrYzdDY+wTDPktbuYjds2LKf9kWYWKwDnHgXU168gdV3sPei7vevFf5m2fPZ6IQSKZkSjHVg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
@@ -314,10 +319,14 @@
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"jassub": ["jassub@2.4.2", "", { "dependencies": { "abslink": "^1.1.6", "lfa-ponyfill": "^1.1.0", "rvfc-polyfill": "^1.0.8", "throughput": "^1.0.2" } }, "sha512-qWECKZWADUYAlXrzTEKPw+zY06XNy4tuZhTTWyk/ML+axRSMuOcbI0tAxL6tdXg+wdROd+EZU83SQFh/frugCg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"lfa-ponyfill": ["lfa-ponyfill@1.1.0", "", {}, "sha512-YS3/DmyDdywWwoEu1ZacAudqkJ4q7WtKE9+bWlaSuEoVrXva7ChIJHMJYs19zyVc1H198pzqAreQU0r/+YNeew=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
@@ -382,6 +391,8 @@
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
"rvfc-polyfill": ["rvfc-polyfill@1.0.8", "", {}, "sha512-uA+0wwTkZ4OT8v45pfDfH+7Yq8mY6MvNngiF5Sq6VBgjJsvsfgt7Q18cyZqZjfAhW9rhkgXPX0cW0R9uw7yElA=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@@ -410,6 +421,8 @@
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"throughput": ["throughput@1.0.2", "", {}, "sha512-jvK1ZXuhsggjb3qYQjMiU/AVYYiTeqT5thWvYR2yuy2LGM84P5MSSyAinwHahGsdBYKR9m9HncVR/3f3nFKkxg=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],

View File

@@ -36,5 +36,8 @@
"vite": "^7.3.1",
"vite-plugin-devtools-json": "^1.0.0",
"wrangler": "^4.81.1"
},
"dependencies": {
"jassub": "^2.4.2"
}
}

View File

@@ -6,6 +6,7 @@ const spacetimedb = schema({
{
id: t.u64().primaryKey(),
url: t.string(),
subtitleUrl: t.string(),
timePosition: t.f64(),
isPlaying: t.bool(),
lastUpdatedAt: t.timestamp(),
@@ -19,6 +20,7 @@ export const init = spacetimedb.init((ctx) => {
ctx.db.videoState.insert({
id: 1n,
url: "https://cdn.cazzzer.com/LycoReco08.mkv",
subtitleUrl: "",
timePosition: 0.0,
isPlaying: false,
lastUpdatedAt: ctx.timestamp,
@@ -34,6 +36,7 @@ export const set_url = spacetimedb.reducer({ url: t.string() }, (ctx, { url }) =
ctx.db.videoState.id.update({
...row,
url,
subtitleUrl: "", // Clear subtitle on new video
timePosition: 0.0,
isPlaying: false,
lastUpdatedAt: ctx.timestamp,
@@ -71,3 +74,13 @@ export const seek = spacetimedb.reducer({ time_position: t.f64() }, (ctx, { time
lastUpdatedAt: ctx.timestamp,
});
});
export const set_subtitle_url = spacetimedb.reducer({ url: t.string() }, (ctx, { url }) => {
const row = ctx.db.videoState.id.find(1n);
if (!row) return;
ctx.db.videoState.id.update({
...row,
subtitleUrl: url,
lastUpdatedAt: ctx.timestamp,
});
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -37,6 +37,7 @@ import {
import PauseReducer from "./pause_reducer";
import PlayReducer from "./play_reducer";
import SeekReducer from "./seek_reducer";
import SetSubtitleUrlReducer from "./set_subtitle_url_reducer";
import SetUrlReducer from "./set_url_reducer";
// Import all procedure arg schemas
@@ -66,6 +67,7 @@ const reducersSchema = __reducers(
__reducerSchema("pause", PauseReducer),
__reducerSchema("play", PlayReducer),
__reducerSchema("seek", SeekReducer),
__reducerSchema("set_subtitle_url", SetSubtitleUrlReducer),
__reducerSchema("set_url", SetUrlReducer),
);

View File

@@ -0,0 +1,15 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
url: __t.string(),
};

View File

@@ -13,6 +13,7 @@ import {
export const VideoState = __t.object("VideoState", {
id: __t.u64(),
url: __t.string(),
subtitleUrl: __t.string(),
timePosition: __t.f64(),
isPlaying: __t.bool(),
lastUpdatedAt: __t.timestamp(),

View File

@@ -9,10 +9,12 @@ import { type Infer as __Infer } from "spacetimedb";
import PauseReducer from "../pause_reducer";
import PlayReducer from "../play_reducer";
import SeekReducer from "../seek_reducer";
import SetSubtitleUrlReducer from "../set_subtitle_url_reducer";
import SetUrlReducer from "../set_url_reducer";
export type PauseParams = __Infer<typeof PauseReducer>;
export type PlayParams = __Infer<typeof PlayReducer>;
export type SeekParams = __Infer<typeof SeekReducer>;
export type SetSubtitleUrlParams = __Infer<typeof SetSubtitleUrlReducer>;
export type SetUrlParams = __Infer<typeof SetUrlReducer>;

View File

@@ -13,6 +13,7 @@ import {
export default __t.row({
id: __t.u64().primaryKey(),
url: __t.string(),
subtitleUrl: __t.string().name("subtitle_url"),
timePosition: __t.f64().name("time_position"),
isPlaying: __t.bool().name("is_playing"),
lastUpdatedAt: __t.timestamp().name("last_updated_at"),

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { useSpacetimeDB, useTable, useReducer } from "spacetimedb/svelte";
import { tables, reducers } from "$lib/st-bindings";
import JASSUB from "jassub";
import font1 from "$lib/assets/fonts/trebuc_0.ttf";
const conn = useSpacetimeDB();
@@ -8,15 +10,36 @@
const videoState = $derived($videoStates.find((state) => state.id === 1n));
const setUrlReducer = useReducer(reducers.setUrl);
const setSubtitleUrlReducer = useReducer(reducers.setSubtitleUrl);
const playReducer = useReducer(reducers.play);
const pauseReducer = useReducer(reducers.pause);
const seekReducer = useReducer(reducers.seek);
let videoElement: HTMLVideoElement | undefined = $state();
let containerElement: HTMLDivElement | undefined = $state();
let newUrl = $state("");
let newSubtitleUrl = $state("");
let isSyncing = false;
let syncTimeout: ReturnType<typeof setTimeout> | undefined;
let jassubInstance: any | undefined;
let currentSubtitleUrl = $derived(videoState?.subtitleUrl);
$effect(() => {
console.log("effect-1");
if (videoElement && currentSubtitleUrl && currentSubtitleUrl.endsWith(".ass")) {
jassubInstance = new JASSUB({
video: videoElement,
subUrl: currentSubtitleUrl,
fonts: [font1],
});
return () => {
jassubInstance?.destroy();
jassubInstance = undefined;
};
}
});
function syncAction(fn: () => void) {
isSyncing = true;
@@ -28,6 +51,7 @@
}
$effect(() => {
console.log("effect-2");
if (!videoElement || !videoState) return;
const el = videoElement;
const state = videoState;
@@ -111,6 +135,21 @@
setUrlReducer({ url: newUrl });
newUrl = "";
}
function handleSetSubtitle(e: SubmitEvent) {
e.preventDefault();
if (!newSubtitleUrl.trim() || !$conn.isActive) return;
setSubtitleUrlReducer({ url: newSubtitleUrl });
newSubtitleUrl = "";
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
containerElement?.requestFullscreen().catch(console.error);
} else {
document.exitFullscreen();
}
}
</script>
<div class="p-4">
@@ -134,19 +173,71 @@
<button type="submit" class="p-2" disabled={!$conn.isActive}>Set URL</button>
</form>
<form onsubmit={handleSetSubtitle} class="mb-2">
<input
type="url"
placeholder="Enter subtitle track URL (.vtt/.ass)"
bind:value={newSubtitleUrl}
class="mr-2 w-96 p-2"
/>
<button type="submit" class="p-2">Set Subtitles</button>
</form>
<div>
<div
bind:this={containerElement}
class="fullscreen-container relative w-full max-w-2xl bg-black"
>
<!-- svelte-ignore a11y_media_has_caption -->
<video
bind:this={videoElement}
muted
controls
controlslist="nofullscreen"
crossorigin="anonymous"
onplay={handlePlay}
onpause={handlePause}
onseeked={handleSeeked}
ontimeupdate={handleTimeUpdate}
class="w-full max-w-2xl bg-black"
ondblclick={toggleFullscreen}
class="h-full w-full"
>
{#if videoState?.subtitleUrl && !videoState.subtitleUrl.endsWith(".ass")}
<track
src={videoState.subtitleUrl}
kind="subtitles"
srclang="en"
label="English"
default
/>
{/if}
Your browser does not support the video tag.
</video>
<button
onclick={toggleFullscreen}
class="absolute top-2 right-2 z-50 rounded bg-black/70 px-2 py-1 text-xs text-white opacity-50 transition-opacity hover:opacity-100"
>
Fullscreen
</button>
</div>
</div>
</div>
<style>
:global(.JASSUB) {
width: 100% !important;
height: 100% !important;
}
:global(.fullscreen-container:fullscreen) {
max-width: none !important;
width: 100vw !important;
height: 100vh !important;
display: flex;
align-items: center;
justify-content: center;
}
:global(.fullscreen-container:fullscreen video) {
width: 100% !important;
height: 100% !important;
}
</style>

View File

@@ -3,4 +3,12 @@ import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [tailwindcss(), sveltekit(), devtoolsJson()] });
export default defineConfig({
plugins: [tailwindcss(), sveltekit(), devtoolsJson()],
optimizeDeps: {
// for some reason this specific combination of
// includes/excludes is required for jassub to work
exclude: ["jassub"],
include: ["throughput"],
}
});