Save video playback position in browser localstorage
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
101
svelte/src/lib/VideoPosition.mts
Normal file
101
svelte/src/lib/VideoPosition.mts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user