Save video playback position in browser localstorage

This commit is contained in:
2025-03-12 17:44:18 +01:00
parent 335012bdb8
commit 00e24879b7
5 changed files with 126 additions and 3 deletions

View File

@@ -3,6 +3,8 @@ import { createEventDispatcher } from "svelte"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import FilePicker from "./FilePicker.svelte" import FilePicker from "./FilePicker.svelte"
import { file_type } from "./FileUtilities.svelte"; import { file_type } from "./FileUtilities.svelte";
import { get_video_position } from "./../lib/VideoPosition.mjs"
import ProgressBar from "./../util/ProgressBar.svelte"
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let list = { export let list = {
@@ -112,6 +114,7 @@ const drop = (e, index) => {
{/if} {/if}
{#each list.files as file, index (file)} {#each list.files as file, index (file)}
{@const vp = get_video_position(file.id)}
<a <a
href="#item={index}" href="#item={index}"
class="file" class="file"
@@ -145,7 +148,13 @@ const drop = (e, index) => {
</button> </button>
</div> </div>
{/if} {/if}
{#if vp !== null}
<div class="grow"></div>
<ProgressBar no_margin used={vp.pos} total={vp.dur}/>
{/if}
</div> </div>
{file.name} {file.name}
</a> </a>
{/each} {/each}
@@ -195,6 +204,8 @@ const drop = (e, index) => {
text-decoration: none; text-decoration: none;
} }
.icon_container { .icon_container {
display: flex;
flex-direction: column;
margin: 3px; margin: 3px;
height: 148px; height: 148px;
border-radius: 6px; border-radius: 6px;
@@ -248,4 +259,7 @@ const drop = (e, index) => {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.grow {
flex: 1 1 auto;
}
</style> </style>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { onMount, createEventDispatcher, tick } from "svelte"; import { onMount, createEventDispatcher, tick } from "svelte";
import { video_position } from "../../lib/VideoPosition.mjs";
import BandwidthUsage from "./BandwidthUsage.svelte"; import BandwidthUsage from "./BandwidthUsage.svelte";
import IconBlock from "../../layout/IconBlock.svelte"; import IconBlock from "../../layout/IconBlock.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -140,6 +141,7 @@ const video_keydown = e => {
on:play={() => playing = true } on:play={() => playing = true }
on:ended={() => dispatch("next", {})} on:ended={() => dispatch("next", {})}
on:keydown={video_keydown} on:keydown={video_keydown}
use:video_position={() => file.id}
> >
<source src={file.get_href} type={file.mime_type} /> <source src={file.get_href} type={file.mime_type} />
</video> </video>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { onMount, createEventDispatcher, tick } from "svelte"; import { onMount, createEventDispatcher, tick } from "svelte";
import { video_position } from "../../lib/VideoPosition.mjs";
import { fs_path_url } from "../FilesystemAPI.mjs"; import { fs_path_url } from "../FilesystemAPI.mjs";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -107,6 +108,7 @@ const video_keydown = e => {
on:play={() => playing = true } on:play={() => playing = true }
on:ended={() => dispatch("open_sibling", 1)} on:ended={() => dispatch("open_sibling", 1)}
on:keydown={video_keydown} on:keydown={video_keydown}
use:video_position={() => $nav.base.sha256_sum.substring(0, 8)}
> >
<source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} /> <source src={fs_path_url($nav.base.path)} type={$nav.base.file_type} />
</video> </video>

View File

@@ -0,0 +1,101 @@
type VideoPositions = {
[key: string]: VideoPosition
}
export type VideoPosition = {
time: number
pos: number
dur: number
}
const storage_key = "video_positions"
const expiry_time = 28 * 24 * 60 * 60 * 1000
let position_cache: VideoPositions | null = null
export const get_video_positions = () => {
if (position_cache !== null) {
return position_cache
}
let video_positions = JSON.parse(window.localStorage.getItem(storage_key)) as VideoPositions
if (video_positions === null) {
return {} as VideoPositions
}
return video_positions
}
export const save_video_position = (id: string, position: number, duration: number) => {
const video_positions = get_video_positions()
// Add our new entry
video_positions[id] = {
time: (new Date).getTime(),
pos: position,
dur: duration,
}
// Remove old entries
const expiry_thresh = (new Date).getTime() - expiry_time
for (const key in video_positions) {
if (video_positions[key].time < expiry_thresh) {
delete video_positions[key]
console.debug("Delete old video position", key)
}
}
// Save updated object
window.localStorage.setItem(storage_key, JSON.stringify(video_positions))
// Update the cache
position_cache = video_positions
}
export const get_video_position = (id: string) => {
const video_positions = get_video_positions()
if (video_positions[id] === undefined) {
return null
}
return video_positions[id]
}
export const video_position = (node: HTMLVideoElement, get_id: () => string) => {
let last_time = 0
const loadeddata = (e: Event) => {
last_time = 0
const vp = get_video_position(get_id())
if (vp === null || vp.pos === 0 || vp.dur === 0) {
return
} else if (vp.pos / vp.dur > 0.95) {
// If the video is more than 95% complete we don't do anything
console.debug("Video is at end, not setting time")
return
}
(e.target as HTMLVideoElement).currentTime = vp.pos
last_time = vp.pos
}
const timeupdate = (e: Event) => {
const vid = (e.target as HTMLVideoElement)
// If the current timestamp is more than ten seconds off the last
// timestamp we saved, then we save the new timestamp
if (Math.abs(vid.currentTime - last_time) > 10) {
save_video_position(get_id(), vid.currentTime, vid.duration)
last_time = vid.currentTime
}
}
node.addEventListener("loadeddata", loadeddata)
node.addEventListener("timeupdate", timeupdate)
return {
destroy() {
node.removeEventListener("loadeddata", loadeddata)
node.removeEventListener("timeupdate", timeupdate)
}
}
}

View File

@@ -4,6 +4,7 @@ export let used = 0
export let animation = "ease" export let animation = "ease"
export let speed = 1000 export let speed = 1000
export let no_animation = false export let no_animation = false
export let no_margin = false
export let style = "" export let style = ""
let percent = 0 let percent = 0
$: { $: {
@@ -21,7 +22,7 @@ $: {
} }
</script> </script>
<div class="progress_bar_outer" style={style}> <div class="progress_bar_outer" style={style} class:no_margin>
<div <div
class="progress_bar_inner" class="progress_bar_inner"
class:no_animation class:no_animation
@@ -49,4 +50,7 @@ $: {
.no_animation { .no_animation {
transition-property: none; transition-property: none;
} }
.no_margin {
margin: 0;
}
</style> </style>