Skip to content
Snippets Groups Projects
Verified Commit d98768bc authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Implement basic seeking

parent cf02a0fb
No related branches found
No related tags found
No related merge requests found
Showing
with 307 additions and 2 deletions
File moved
import {createContext, useContext} from "react";
import {Locale} from "./Locale";
const LocalizedContext = createContext<Locale>({
language: "en",
region: "US",
});
export const LocalizedProvider = LocalizedContext.Provider;
export const LocalizedConsumer = LocalizedContext.Consumer;
export const useLocale = () => useContext(LocalizedContext);
File moved
File moved
function warn(message: string) {
console.debug(`Media Fragments URI Parsing Warning: ${message}`);
}
// the currently supported media fragments dimensions are: t, xywh, track, id
// allows for O(1) checks for existence of valid keys
const dimensions: { [key:string]: (value: string) => keyValuePairs | false } = {
t: function (value: string): keyValuePairs | false {
const npt = /^(?:(?:npt:)?(?:(?:(\d+):)?(\d\d):)?(\d+(?:\.\d*)?)?)$/;
const components = value.split(',');
if (components.length > 2) {
return false;
}
const start = components[0] ? components[0] : '';
const end = components[1] ? components[1] : '';
if ((start === '' && end === '') || (start && !end && value.indexOf(',') !== -1)) {
return false;
}
const matchStart = npt.exec(start);
const matchEnd = npt.exec(end);
if (matchStart !== null && matchEnd !== null) {
const startNormalized = convertToSeconds(matchStart);
const endNormalized = convertToSeconds(matchEnd);
if (start && end) {
if (startNormalized < endNormalized) {
return {
value: value,
unit: 'npt',
start: start,
end: end,
startNormalized: startNormalized === false ? undefined : startNormalized,
endNormalized: endNormalized === false ? undefined : endNormalized
};
} else {
warn('Please ensure that start < end.');
return false;
}
} else {
if ((convertToSeconds(matchStart) !== false) || (convertToSeconds(matchEnd) !== false)) {
return {
value: value,
unit: 'npt',
start: start,
end: end,
startNormalized: startNormalized === false ? undefined : startNormalized,
endNormalized: endNormalized === false ? undefined : endNormalized
};
} else {
warn('Please ensure that start or end are legal.');
return false;
}
}
}
warn('Invalid time dimension.');
return false;
}, xywh: function (value: string): keyValuePairs | false {
const xywh = /^(?:(pixel|percent):)?(\d+),(\d+),(\d+),(\d+)$/;
const match = xywh.exec(value);
if (!match) {
return false;
}
const type = match[1] || 'pixel';
const x = parseInt(match[2], 10);
const y = parseInt(match[3], 10);
const w = parseInt(match[4], 10);
const h = parseInt(match[5], 10);
if (type === 'pixel') {
if (w > 0 && h > 0) {
return {
value: value, unit: 'pixel', x: x, y: y, w: w, h: h
};
} else {
warn('Please ensure that w > 0 and h > 0');
return false;
}
} else if (type === 'percent') {
if (checkPercentSelection(x, y, w, h)) {
return {
value: value, unit: 'percent', x: x, y: y, w: w, h: h
};
}
warn('Invalid percent selection.');
return false;
} else {
warn('Invalid spatial dimension.');
return false;
}
}, track: function (value: string) {
return {
value: value, name: value
};
}, id: function (value: string) {
return {
value: value, name: value
};
}, chapter: function (value: string) {
return {
value: value, chapter: value
};
}
};
/**
* checks for valid percent selections
*/
function checkPercentSelection(x: number, y: number, w: number, h: number): boolean {
if (!((0 <= x) && (x <= 100))) {
warn('Please ensure that 0 <= x <= 100.');
return false;
}
if (!((0 <= y) && (y <= 100))) {
warn('Please ensure that 0 <= y <= 100.');
return false;
}
if (!((0 <= w) && (w <= 100))) {
warn('Please ensure that 0 <= w <= 100.');
return false;
}
if (!((0 <= h) && (h <= 100))) {
warn('Please ensure that 0 <= h <= 100.');
return false;
}
if (x + w > 100) {
warn('Please ensure that x + w <= 100.');
return false;
}
if (y + h > 100) {
warn('Please ensure that y + h <= 100.');
return false;
}
return true;
}
function convertToSeconds(match: RegExpMatchArray): number | false {
const hours = parseInt(match[0] || "0", 10);
const minutes = parseInt(match[1] || "0", 10);
const seconds = parseFloat(match[2]);
if (hours > 23) {
warn('Please ensure that hours <= 23.');
return false;
}
if (minutes > 59) {
warn('Please ensure that minutes <= 59.');
return false;
}
if (hours !== 0 && minutes !== 0 && seconds >= 60) {
// this constraint must not be applied if you specify only seconds
warn('Please ensure that seconds < 60.');
return false;
}
return hours * 3600 + minutes * 60 + seconds;
}
function splitKeyValuePairs(fragment: string): { [key:string]: keyValuePairs | false } {
const params: { [key:string]: keyValuePairs | false } = {};
fragment.split('&').forEach((hash: string) => {
const [key, val] = hash.split('=', 2);
if (Object.keys(dimensions).includes(key)) {
params[key] = dimensions[key](decodeURIComponent(val));
}
});
return params;
}
interface keyValuePairs {
value?: string,
unit?: string,
x?: number,
y?: number,
w?: number,
h?: number,
start?: string,
startNormalized?: number,
end?: string,
endNormalized?: number,
name?: string,
chapter?: string,
}
function parse(optional_uri: string): { [key:string]: keyValuePairs | false } {
return splitKeyValuePairs(new URL(optional_uri || window.location.href)?.hash?.slice(1));
}
export default parse;
import {useLayoutEffect, useRef, useState} from "react";
export const useCanvas = (width: number | null, height: number | null): [HTMLCanvasElement, CanvasRenderingContext2D | null] => {
const canvas = useRef(document.createElement("canvas"));
const [context, setContext] = useState<CanvasRenderingContext2D | null>(null);
useLayoutEffect(() => {
if (width !== null && height !== null) {
canvas.current.width = width;
canvas.current.height = height;
setContext(canvas.current.getContext("2d"));
}
}, [width, height, setContext]);
return [canvas.current, context];
}
import {useEffect, useState} from "react";
export const useDuration = (video: HTMLVideoElement | null) => {
const [duration, setDuration] = useState<number>(0);
useEffect(() => {
if (video !== null) {
if (video.readyState >= 1) {
setDuration(video.duration);
} else {
const listener = () => {
window.requestAnimationFrame(() => {
setDuration(video.duration);
})
};
video.addEventListener("loadedmetadata", listener)
return () => {
video.removeEventListener("loadedmetadata", listener)
}
}
}
}, [video]);
return duration;
}
import {useEffect, useRef, useState} from "react";
export const useImage = (url: string | null): HTMLImageElement | null => {
const image = useRef(new Image())
image.current.setAttribute("crossorigin", "anonymous");
const [imageData, setImageData] = useState<HTMLImageElement | null>(null);
useEffect(() => {
try {
image.current.src = url || "";
image.current.decode().then(() => {
setImageData(image.current);
})
} catch (_) {
}
}, [url]);
return imageData;
}
import {useEffect, useState} from "react";
export const usePosition = (video: HTMLVideoElement | null) => {
const [position, setPosition] = useState<number>(0);
useEffect(() => {
if (video !== null) {
const listener = () => {
window.requestAnimationFrame(() => {
setPosition(video.currentTime);
})
};
video.addEventListener("progress", listener)
return () => {
video.removeEventListener("progress", listener)
}
}
}, [video]);
return position;
}
import {useEffect, useState} from "react";
export const useTextTrackCues = (track: HTMLTrackElement) => {
const [cues, setCues] = useState<TextTrackCue[]>([]);
useEffect(() => {
track.track.mode = "hidden";
if (track.readyState >= 1) {
setCues(Array.from(track.track.cues || []))
} else {
const listener = () => {
window.requestAnimationFrame(() => {
setCues(Array.from(track.track.cues || []))
})
};
track.addEventListener("load", listener)
return () => {
track.removeEventListener("load", listener)
}
}
}, [track]);
return cues;
}
File moved
import {useRef} from "react"; import {useRef} from "react";
import {Media} from "../api/models/Media"; import {Media} from "../../api/models/Media";
import {sortNumericallyDesc} from "../sort/sortNumerically";
import {PlayabilityRating} from "./PlayabilityRating"; import {PlayabilityRating} from "./PlayabilityRating";
import {videoMimeString} from "./videoMimeString"; import {videoMimeString} from "./videoMimeString";
...@@ -16,3 +17,10 @@ export const usePlayabilityRating = () => { ...@@ -16,3 +17,10 @@ export const usePlayabilityRating = () => {
} }
} }
} }
export const selectPlayabileMedia = (
playabilityRating: (media: Media) => PlayabilityRating,
media: Media[]
): Media | null =>
media.sort(sortNumericallyDesc(it => playabilityRating(it)))[0]
|| null
import {Media} from "../api/models/Media"; import {Media} from "../../api/models/Media";
export const videoMimeString = (media: Media) => export const videoMimeString = (media: Media) =>
`${media.mime}; codecs="${media.codecs.join(", ")}"`; `${media.mime}; codecs="${media.codecs.join(", ")}"`;
File moved
File moved
File moved
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment