diff --git a/spacetimedb/src/index.ts b/spacetimedb/src/index.ts index f3238d4..c4e2297 100644 --- a/spacetimedb/src/index.ts +++ b/spacetimedb/src/index.ts @@ -1,34 +1,73 @@ import { schema, table, t } from "spacetimedb/server"; const spacetimedb = schema({ - person: table( + videoState: table( { public: true }, { - name: t.string(), + id: t.u64().primaryKey(), + url: t.string(), + timePosition: t.f64(), + isPlaying: t.bool(), + lastUpdatedAt: t.timestamp(), }, ), }); export default spacetimedb; -export const init = spacetimedb.init((_ctx) => { - // Called when the module is initially published +export const init = spacetimedb.init((ctx) => { + // Insert initial video state row on first deploy + ctx.db.videoState.insert({ + id: 1n, + url: "https://cdn.cazzzer.com/LycoReco08.mkv", + timePosition: 0.0, + isPlaying: false, + lastUpdatedAt: ctx.timestamp, + }); }); -export const onConnect = spacetimedb.clientConnected((_ctx) => { - // Called every time a new client connects +export const onConnect = spacetimedb.clientConnected((_ctx) => {}); +export const onDisconnect = spacetimedb.clientDisconnected((_ctx) => {}); + +export const set_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, + url, + timePosition: 0.0, + isPlaying: false, + lastUpdatedAt: ctx.timestamp, + }); }); -export const onDisconnect = spacetimedb.clientDisconnected((_ctx) => { - // Called every time a client disconnects +export const play = spacetimedb.reducer({ time_position: t.f64() }, (ctx, { time_position }) => { + const row = ctx.db.videoState.id.find(1n); + if (!row) return; + ctx.db.videoState.id.update({ + ...row, + timePosition: time_position, + isPlaying: true, + lastUpdatedAt: ctx.timestamp, + }); }); -export const add = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { - ctx.db.person.insert({ name }); +export const pause = spacetimedb.reducer({ time_position: t.f64() }, (ctx, { time_position }) => { + const row = ctx.db.videoState.id.find(1n); + if (!row) return; + ctx.db.videoState.id.update({ + ...row, + timePosition: time_position, + isPlaying: false, + lastUpdatedAt: ctx.timestamp, + }); }); -export const sayHello = spacetimedb.reducer((ctx) => { - for (const person of ctx.db.person.iter()) { - console.info(`Hello, ${person.name}!`); - } - console.info("Hello, World!"); +export const seek = spacetimedb.reducer({ time_position: t.f64() }, (ctx, { time_position }) => { + const row = ctx.db.videoState.id.find(1n); + if (!row) return; + ctx.db.videoState.id.update({ + ...row, + timePosition: time_position, + lastUpdatedAt: ctx.timestamp, + }); }); diff --git a/src/lib/st-bindings/index.ts b/src/lib/st-bindings/index.ts index 3311423..0905896 100644 --- a/src/lib/st-bindings/index.ts +++ b/src/lib/st-bindings/index.ts @@ -34,31 +34,39 @@ import { } from "spacetimedb"; // Import all reducer arg schemas -import AddReducer from "./add_reducer"; -import SayHelloReducer from "./say_hello_reducer"; +import PauseReducer from "./pause_reducer"; +import PlayReducer from "./play_reducer"; +import SeekReducer from "./seek_reducer"; +import SetUrlReducer from "./set_url_reducer"; // Import all procedure arg schemas // Import all table schema definitions -import PersonRow from "./person_table"; +import VideoStateRow from "./video_state_table"; /** Type-only namespace exports for generated type groups. */ /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema({ - person: __table({ - name: 'person', + videoState: __table({ + name: 'video_state', indexes: [ + { accessor: 'id', name: 'video_state_id_idx_btree', algorithm: 'btree', columns: [ + 'id', + ] }, ], constraints: [ + { name: 'video_state_id_key', constraint: 'unique', columns: ['id'] }, ], - }, PersonRow), + }, VideoStateRow), }); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema("add", AddReducer), - __reducerSchema("say_hello", SayHelloReducer), + __reducerSchema("pause", PauseReducer), + __reducerSchema("play", PlayReducer), + __reducerSchema("seek", SeekReducer), + __reducerSchema("set_url", SetUrlReducer), ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ diff --git a/src/lib/st-bindings/say_hello_reducer.ts b/src/lib/st-bindings/pause_reducer.ts similarity index 87% rename from src/lib/st-bindings/say_hello_reducer.ts rename to src/lib/st-bindings/pause_reducer.ts index e18fbc0..fad0b2c 100644 --- a/src/lib/st-bindings/say_hello_reducer.ts +++ b/src/lib/st-bindings/pause_reducer.ts @@ -10,4 +10,6 @@ import { type Infer as __Infer, } from "spacetimedb"; -export default {}; +export default { + timePosition: __t.f64(), +}; diff --git a/src/lib/st-bindings/person_table.ts b/src/lib/st-bindings/play_reducer.ts similarity index 86% rename from src/lib/st-bindings/person_table.ts rename to src/lib/st-bindings/play_reducer.ts index 4dc4a82..fad0b2c 100644 --- a/src/lib/st-bindings/person_table.ts +++ b/src/lib/st-bindings/play_reducer.ts @@ -10,6 +10,6 @@ import { type Infer as __Infer, } from "spacetimedb"; -export default __t.row({ - name: __t.string(), -}); +export default { + timePosition: __t.f64(), +}; diff --git a/src/lib/st-bindings/seek_reducer.ts b/src/lib/st-bindings/seek_reducer.ts new file mode 100644 index 0000000..fad0b2c --- /dev/null +++ b/src/lib/st-bindings/seek_reducer.ts @@ -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 { + timePosition: __t.f64(), +}; diff --git a/src/lib/st-bindings/add_reducer.ts b/src/lib/st-bindings/set_url_reducer.ts similarity index 94% rename from src/lib/st-bindings/add_reducer.ts rename to src/lib/st-bindings/set_url_reducer.ts index ce493ee..b0eb16a 100644 --- a/src/lib/st-bindings/add_reducer.ts +++ b/src/lib/st-bindings/set_url_reducer.ts @@ -11,5 +11,5 @@ import { } from "spacetimedb"; export default { - name: __t.string(), + url: __t.string(), }; diff --git a/src/lib/st-bindings/types.ts b/src/lib/st-bindings/types.ts index b21f984..45704fe 100644 --- a/src/lib/st-bindings/types.ts +++ b/src/lib/st-bindings/types.ts @@ -10,8 +10,12 @@ import { type Infer as __Infer, } from "spacetimedb"; -export const Person = __t.object("Person", { - name: __t.string(), +export const VideoState = __t.object("VideoState", { + id: __t.u64(), + url: __t.string(), + timePosition: __t.f64(), + isPlaying: __t.bool(), + lastUpdatedAt: __t.timestamp(), }); -export type Person = __Infer; +export type VideoState = __Infer; diff --git a/src/lib/st-bindings/types/reducers.ts b/src/lib/st-bindings/types/reducers.ts index c4c5aac..7860baf 100644 --- a/src/lib/st-bindings/types/reducers.ts +++ b/src/lib/st-bindings/types/reducers.ts @@ -6,9 +6,13 @@ import { type Infer as __Infer } from "spacetimedb"; // Import all reducer arg schemas -import AddReducer from "../add_reducer"; -import SayHelloReducer from "../say_hello_reducer"; +import PauseReducer from "../pause_reducer"; +import PlayReducer from "../play_reducer"; +import SeekReducer from "../seek_reducer"; +import SetUrlReducer from "../set_url_reducer"; -export type AddParams = __Infer; -export type SayHelloParams = __Infer; +export type PauseParams = __Infer; +export type PlayParams = __Infer; +export type SeekParams = __Infer; +export type SetUrlParams = __Infer; diff --git a/src/lib/st-bindings/video_state_table.ts b/src/lib/st-bindings/video_state_table.ts new file mode 100644 index 0000000..ca77a49 --- /dev/null +++ b/src/lib/st-bindings/video_state_table.ts @@ -0,0 +1,19 @@ +// 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 __t.row({ + id: __t.u64().primaryKey(), + url: __t.string(), + timePosition: __t.f64().name("time_position"), + isPlaying: __t.bool().name("is_playing"), + lastUpdatedAt: __t.timestamp().name("last_updated_at"), +}); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cf60de3..1899a37 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -39,5 +39,8 @@ createSpacetimeDBProvider(connectionBuilder); - + + + Space Stream + {@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 57029ab..ccf81d5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,56 +4,114 @@ const conn = useSpacetimeDB(); - // Subscribe to all people in the database - const [people] = useTable(tables.person); + const [videoStates] = useTable(tables.videoState); + const videoState = $derived($videoStates.find((state) => state.id === 1n)); - const addReducer = useReducer(reducers.add); + const setUrlReducer = useReducer(reducers.setUrl); + const playReducer = useReducer(reducers.play); + const pauseReducer = useReducer(reducers.pause); + const seekReducer = useReducer(reducers.seek); - let name = $state(""); + let videoElement: HTMLVideoElement | undefined = $state(); + let newUrl = $state(""); - function addPerson(e: SubmitEvent) { + let ignoreNextEvent = false; + + $effect(() => { + if (!videoElement || !videoState) return; + const state = videoState; + + if (videoElement.src !== state.url) { + videoElement.src = state.url; + } + + // Account for server to client clock drift slightly, but MVP assumes relatively synced clocks. + const timeElapsedMicros = BigInt(Date.now()) * 1000n - state.lastUpdatedAt.microsSinceUnixEpoch; + const expectedTime = state.isPlaying + ? state.timePosition + Number(timeElapsedMicros) / 1_000_000 + : state.timePosition; + + const diff = Math.abs(videoElement.currentTime - expectedTime); + + // Allow up to 2 seconds drift without snapping + if (diff > 2.0) { + ignoreNextEvent = true; + videoElement.currentTime = expectedTime; + } + + if (state.isPlaying && videoElement.paused) { + ignoreNextEvent = true; + videoElement.play().catch(console.error); + } else if (!state.isPlaying && !videoElement.paused) { + ignoreNextEvent = true; + videoElement.pause(); + } + }); + + function handlePlay() { + if (ignoreNextEvent) { + ignoreNextEvent = false; + return; + } + playReducer({ timePosition: videoElement?.currentTime ?? 0 }); + } + + function handlePause() { + if (ignoreNextEvent) { + ignoreNextEvent = false; + return; + } + pauseReducer({ timePosition: videoElement?.currentTime ?? 0 }); + } + + function handleSeeked() { + if (ignoreNextEvent) { + ignoreNextEvent = false; + return; + } + seekReducer({ timePosition: videoElement?.currentTime ?? 0 }); + } + + function handleSetUrl(e: SubmitEvent) { e.preventDefault(); - if (!name.trim() || !$conn.isActive) return; - - // Call the add reducer - addReducer({ name: name }); - name = ""; + if (!newUrl.trim() || !$conn.isActive) return; + setUrlReducer({ url: newUrl }); + newUrl = ""; } -
-

SpacetimeDB Svelte App

+
+

Space Stream

-
+
Status: {$conn.isActive ? "Connected" : "Disconnected"}
-
+ - +
-

People ({$people.length})

- {#if $people.length === 0} -

No people yet. Add someone above!

- {:else} -
    - {#each $people as person} -
  • {person.name}
  • - {/each} -
- {/if} + +