From 6227426b01271694bbb85eab6fa381d5cfd19fa0 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Thu, 26 Nov 2020 22:42:39 +0100
Subject: [PATCH] Implement basic DASH handling

---
 package-lock.json                             |  78 +++++++++++
 package.json                                  |   3 +
 src/App.tsx                                   |   8 +-
 src/api/ApiClient.ts                          |  22 +--
 src/api/ApiClientContext.ts                   |   4 +-
 src/api/models/Content.ts                     |  48 +++++++
 ...leDescription.ts => ContentDescription.ts} |   2 +-
 src/api/models/{TitleId.ts => ContentId.ts}   |   2 +-
 .../models/{TitleKind.ts => ContentKind.ts}   |   2 +-
 .../models/{TitleName.ts => ContentName.ts}   |   2 +-
 src/api/models/{Episode.ts => Instalment.ts}  |   6 +-
 src/api/models/Title.ts                       |  48 -------
 src/api/models/dto/ContentMeta.ts             |   7 +
 src/api/models/dto/GenreWithContent.ts        |   7 +
 src/api/models/dto/GenreWithTitles.ts         |   7 -
 src/api/models/dto/TitleMeta.ts               |   7 -
 src/routes/ContentRoute.tsx                   |  21 +++
 src/routes/TitleRoute.tsx                     |  21 ---
 src/routes/main/MainPage.tsx                  |  30 ++--
 src/routes/player/Player.tsx                  |  62 +++++----
 src/routes/player/PlayerPage.tsx              |  12 +-
 src/routes/player/SeekBar.tsx                 |   5 +-
 src/routes/player/video/DashVideoElement.tsx  | 129 ++++++++++++++++++
 src/routes/player/video/RawVideoElement.tsx   | 129 ++++++++++++++++++
 src/routes/player/video/VideoApi.ts           |  35 +++++
 src/routes/player/video/VideoElement.tsx      |  23 ++++
 src/util/CurrentContentContext.ts             |   7 +
 src/util/CurrentTitleContext.ts               |   7 -
 src/util/media/useAudioTracks.ts              |  25 ++++
 src/util/media/useDebugInfo.ts                |  22 +++
 src/util/media/useDuration.ts                 |  13 +-
 src/util/media/usePosition.ts                 |   9 +-
 32 files changed, 632 insertions(+), 171 deletions(-)
 create mode 100644 src/api/models/Content.ts
 rename src/api/models/{TitleDescription.ts => ContentDescription.ts} (72%)
 rename src/api/models/{TitleId.ts => ContentId.ts} (76%)
 rename src/api/models/{TitleKind.ts => ContentKind.ts} (71%)
 rename src/api/models/{TitleName.ts => ContentName.ts} (70%)
 rename src/api/models/{Episode.ts => Instalment.ts} (50%)
 delete mode 100644 src/api/models/Title.ts
 create mode 100644 src/api/models/dto/ContentMeta.ts
 create mode 100644 src/api/models/dto/GenreWithContent.ts
 delete mode 100644 src/api/models/dto/GenreWithTitles.ts
 delete mode 100644 src/api/models/dto/TitleMeta.ts
 create mode 100644 src/routes/ContentRoute.tsx
 delete mode 100644 src/routes/TitleRoute.tsx
 create mode 100644 src/routes/player/video/DashVideoElement.tsx
 create mode 100644 src/routes/player/video/RawVideoElement.tsx
 create mode 100644 src/routes/player/video/VideoApi.ts
 create mode 100644 src/routes/player/video/VideoElement.tsx
 create mode 100644 src/util/CurrentContentContext.ts
 delete mode 100644 src/util/CurrentTitleContext.ts
 create mode 100644 src/util/media/useAudioTracks.ts
 create mode 100644 src/util/media/useDebugInfo.ts

diff --git a/package-lock.json b/package-lock.json
index 24eba99..833b9fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3673,6 +3673,11 @@
         "q": "^1.1.2"
       }
     },
+    "codem-isoboxer": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz",
+      "integrity": "sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw=="
+    },
     "collection-visit": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -4341,6 +4346,27 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "dashjs": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-3.1.3.tgz",
+      "integrity": "sha512-eymNYAr1KTMNTuq9k1W9UPwkFvWvpR+ykKTXQnPnD/W00DVFqdl4bZ1B4MUdFHFE3H38Bij1/cigRnDydXJvsQ==",
+      "requires": {
+        "codem-isoboxer": "0.3.6",
+        "fast-deep-equal": "2.0.1",
+        "html-entities": "^1.2.1",
+        "imsc": "^1.0.2",
+        "localforage": "^1.7.1",
+        "request": "^2.87.0",
+        "request-promise": "^4.2.2"
+      },
+      "dependencies": {
+        "fast-deep-equal": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+          "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+        }
+      }
+    },
     "data-urls": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
@@ -6684,6 +6710,11 @@
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
     },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+    },
     "immer": {
       "version": "7.0.9",
       "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz",
@@ -6723,6 +6754,21 @@
         "resolve-cwd": "^2.0.0"
       }
     },
+    "imsc": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.1.tgz",
+      "integrity": "sha512-kfWjrmg/vdcqM65FPxpq46RyxKTpfMikDk0PhXOeAlZ6o1OkWBZsll4TlmSj261WcuNT252VBB0aGqSQ+vBZ/A==",
+      "requires": {
+        "sax": "1.2.1"
+      },
+      "dependencies": {
+        "sax": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+          "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
+        }
+      }
+    },
     "imurmurhash": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -8112,6 +8158,14 @@
         "type-check": "~0.3.2"
       }
     },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
     "lines-and-columns": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -8199,6 +8253,14 @@
         }
       }
     },
+    "localforage": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
+      "integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
+      "requires": {
+        "lie": "3.1.1"
+      }
+    },
     "locate-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@@ -11003,6 +11065,11 @@
         "tiny-warning": "^1.0.2"
       }
     },
+    "react-merge-refs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
+      "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ=="
+    },
     "react-router": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
@@ -11382,6 +11449,17 @@
         "uuid": "^3.3.2"
       }
     },
+    "request-promise": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz",
+      "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==",
+      "requires": {
+        "bluebird": "^3.5.0",
+        "request-promise-core": "1.1.4",
+        "stealthy-require": "^1.1.1",
+        "tough-cookie": "^2.3.3"
+      }
+    },
     "request-promise-core": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
diff --git a/package.json b/package.json
index c90d4d7..6e2091d 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,12 @@
     "@types/react-dom": "^16.9.8",
     "@types/react-router": "^5.1.8",
     "@types/react-router-dom": "^5.1.5",
+    "dashjs": "^3.1.3",
     "immer": "^7.0.9",
     "react": "^16.13.1",
     "react-dom": "^16.13.1",
     "react-jss": "^10.4.0",
+    "react-merge-refs": "^1.1.0",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "3.4.3",
@@ -29,6 +31,7 @@
     "test": "react-scripts test",
     "eject": "react-scripts eject"
   },
+  "proxy": "http://localhost:8000",
   "eslintConfig": {
     "extends": "react-app"
   },
diff --git a/src/App.tsx b/src/App.tsx
index b61c403..46aa883 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,15 +2,15 @@ import React from 'react';
 import {BrowserRouter, Route} from 'react-router-dom';
 import {MainPage} from "./routes/main/MainPage";
 import {PlayerPage} from "./routes/player/PlayerPage";
-import {TitleRoute} from "./routes/TitleRoute";
+import {ContentRoute} from "./routes/ContentRoute";
 
 export function App() {
     return (
         <BrowserRouter>
-            <Route path="/player/:titleId" exact>
-                <TitleRoute>
+            <Route path="/player/:contentId" exact>
+                <ContentRoute>
                     <PlayerPage/>
-                </TitleRoute>
+                </ContentRoute>
             </Route>
             <Route path="/" exact>
                 <MainPage/>
diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts
index 91d3e36..972326f 100644
--- a/src/api/ApiClient.ts
+++ b/src/api/ApiClient.ts
@@ -1,9 +1,9 @@
 import {RequestClient} from "../util/request/RequestClient";
-import {GenreWithTitles} from "./models/dto/GenreWithTitles";
-import {TitleMeta} from "./models/dto/TitleMeta";
-import {Episode} from "./models/Episode";
+import {GenreWithContent} from "./models/dto/GenreWithContent";
+import {ContentMeta} from "./models/dto/ContentMeta";
+import {Instalment} from "./models/Instalment";
 import {Genre} from "./models/Genre";
-import {Title} from "./models/Title";
+import {Content} from "./models/Content";
 
 export class ApiClient extends RequestClient {
     public async listGenres(): Promise<Genre[]> {
@@ -12,26 +12,26 @@ export class ApiClient extends RequestClient {
         });
     }
 
-    public async getGenre(genreId: string): Promise<GenreWithTitles> {
+    public async getGenre(genreId: string): Promise<GenreWithContent> {
         return await this.request(`api/v1/genres/${genreId}`, {
             method: "GET"
         });
     }
 
-    public async listTitles(): Promise<Title[]> {
-        return await this.request(`api/v1/titles`, {
+    public async listContent(): Promise<Content[]> {
+        return await this.request(`api/v1/content`, {
             method: "GET"
         });
     }
 
-    public async getTitle(titleId: string): Promise<TitleMeta> {
-        return await this.request(`api/v1/titles/${titleId}`, {
+    public async getContent(contentId: string): Promise<ContentMeta> {
+        return await this.request(`api/v1/content/${contentId}`, {
             method: "GET"
         });
     }
 
-    public async listEpisodes(titleId: string): Promise<Episode> {
-        return await this.request(`api/v1/titles/${titleId}/episodes`, {
+    public async listEpisodes(contentId: string): Promise<Instalment> {
+        return await this.request(`api/v1/content/${contentId}/episodes`, {
             method: "GET"
         });
     }
diff --git a/src/api/ApiClientContext.ts b/src/api/ApiClientContext.ts
index 22e326d..f8f2618 100644
--- a/src/api/ApiClientContext.ts
+++ b/src/api/ApiClientContext.ts
@@ -8,9 +8,7 @@ interface ApiClientContext {
 const ApiClientContext = createContext<ApiClientContext>({
     apiClient: new ApiClient(
         localStorage.getItem("API_ENDPOINT") ||
-        process.env.NODE_ENV === "development"
-            ? "http://localhost:8000/"
-            : new URL("/", window.location.href).toString()
+        new URL("/", window.location.href).toString()
     )
 });
 
diff --git a/src/api/models/Content.ts b/src/api/models/Content.ts
new file mode 100644
index 0000000..f31b39c
--- /dev/null
+++ b/src/api/models/Content.ts
@@ -0,0 +1,48 @@
+import {Locale} from "../../util/locale/Locale";
+import {LocalePriority} from "../../util/locale/LocalePriority";
+import {selectLocaleVersion} from "../../util/locale/selectLocaleData";
+import {selectRegionalVersion} from "../../util/locale/selectRegionalData";
+import {PlayabilityRating} from "../../util/mime/PlayabilityRating";
+import {selectPlayabileMedia} from "../../util/mime/usePlayabilityRating";
+import {Cast} from "./Cast";
+import {Genre} from "./Genre";
+import {Image} from "./Image";
+import {Media} from "./Media";
+import {Rating} from "./Rating";
+import {Subtitle} from "./Subtitle";
+import {ContentDescription} from "./ContentDescription";
+import {ContentId} from "./ContentId";
+import {ContentKind} from "./ContentKind";
+import {ContentName} from "./ContentName";
+
+export interface Content {
+    ids: ContentId,
+    kind: ContentKind,
+    originalLanguage: string | null,
+    runtime: number | null,
+    yearStart: number | null,
+    yearEnd: number | null,
+    titles: ContentName[],
+    descriptions: ContentDescription[],
+    cast: Cast[],
+    genres: Genre[],
+    ratings: Rating[],
+    images: Image[],
+    media: Media[],
+    subtitles: Subtitle[],
+    preview: string | null,
+    createdAt: string,
+    updatedAt: string
+}
+
+export const getLocalizedName = (content: Content, locale: Locale): ContentName | null =>
+    selectLocaleVersion(locale, LocalePriority.REGION, content.titles)
+
+export const getLocalizedDescription = (content: Content, locale: Locale): ContentDescription | null =>
+    selectLocaleVersion(locale, LocalePriority.LOCALE, content.descriptions)
+
+export const getLocalizedRating = (content: Content, locale: Locale): Rating | null =>
+    selectRegionalVersion(locale, content.ratings)
+
+export const getPlayableMedia = (content: Content, playabilityRating: (media: Media) => PlayabilityRating): Media | null =>
+    selectPlayabileMedia(playabilityRating, content.media)
diff --git a/src/api/models/TitleDescription.ts b/src/api/models/ContentDescription.ts
similarity index 72%
rename from src/api/models/TitleDescription.ts
rename to src/api/models/ContentDescription.ts
index 3a8a94f..1c8b0b4 100644
--- a/src/api/models/TitleDescription.ts
+++ b/src/api/models/ContentDescription.ts
@@ -1,6 +1,6 @@
 import {LocalizedData} from "../../util/locale/LocalizedData";
 
-export interface TitleDescription extends LocalizedData {
+export interface ContentDescription extends LocalizedData {
     overview: string,
     tagline: string | null,
     languages: string[],
diff --git a/src/api/models/TitleId.ts b/src/api/models/ContentId.ts
similarity index 76%
rename from src/api/models/TitleId.ts
rename to src/api/models/ContentId.ts
index 249afde..e0992d8 100644
--- a/src/api/models/TitleId.ts
+++ b/src/api/models/ContentId.ts
@@ -1,4 +1,4 @@
-export interface TitleId {
+export interface ContentId {
     uuid: string,
     imdb: string | null,
     tmdb: number | null,
diff --git a/src/api/models/TitleKind.ts b/src/api/models/ContentKind.ts
similarity index 71%
rename from src/api/models/TitleKind.ts
rename to src/api/models/ContentKind.ts
index be3f792..79bc107 100644
--- a/src/api/models/TitleKind.ts
+++ b/src/api/models/ContentKind.ts
@@ -1,4 +1,4 @@
-export enum TitleKind {
+export enum ContentKind {
     Movie = "movie",
     Show = "show",
     Episode = "episode"
diff --git a/src/api/models/TitleName.ts b/src/api/models/ContentName.ts
similarity index 70%
rename from src/api/models/TitleName.ts
rename to src/api/models/ContentName.ts
index 98f66b5..a148037 100644
--- a/src/api/models/TitleName.ts
+++ b/src/api/models/ContentName.ts
@@ -1,6 +1,6 @@
 import {LocalizedData} from "../../util/locale/LocalizedData";
 
-export interface TitleName extends LocalizedData {
+export interface ContentName extends LocalizedData {
     name: string,
     languages: string[],
     kind: string
diff --git a/src/api/models/Episode.ts b/src/api/models/Instalment.ts
similarity index 50%
rename from src/api/models/Episode.ts
rename to src/api/models/Instalment.ts
index 2463d61..b322709 100644
--- a/src/api/models/Episode.ts
+++ b/src/api/models/Instalment.ts
@@ -1,8 +1,8 @@
-import {Title} from "./Title";
+import {Content} from "./Content";
 
-export interface Episode {
+export interface Instalment {
     season: string | null,
     episode: string | null,
     airDate: string | null,
-    title: Title
+    content: Content
 }
diff --git a/src/api/models/Title.ts b/src/api/models/Title.ts
deleted file mode 100644
index 5e0caec..0000000
--- a/src/api/models/Title.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import {Locale} from "../../util/locale/Locale";
-import {LocalePriority} from "../../util/locale/LocalePriority";
-import {selectLocaleVersion} from "../../util/locale/selectLocaleData";
-import {selectRegionalVersion} from "../../util/locale/selectRegionalData";
-import {PlayabilityRating} from "../../util/mime/PlayabilityRating";
-import {selectPlayabileMedia} from "../../util/mime/usePlayabilityRating";
-import {Cast} from "./Cast";
-import {Genre} from "./Genre";
-import {Image} from "./Image";
-import {Media} from "./Media";
-import {Rating} from "./Rating";
-import {Subtitle} from "./Subtitle";
-import {TitleDescription} from "./TitleDescription";
-import {TitleId} from "./TitleId";
-import {TitleKind} from "./TitleKind";
-import {TitleName} from "./TitleName";
-
-export interface Title {
-    ids: TitleId,
-    kind: TitleKind,
-    originalLanguage: string | null,
-    runtime: number | null,
-    yearStart: number | null,
-    yearEnd: number | null,
-    titles: TitleName[],
-    descriptions: TitleDescription[],
-    cast: Cast[],
-    genres: Genre[],
-    ratings: Rating[],
-    images: Image[],
-    media: Media[],
-    subtitles: Subtitle[],
-    preview: string | null,
-    createdAt: string,
-    updatedAt: string
-}
-
-export const getLocalizedName = (title: Title, locale: Locale): TitleName | null =>
-    selectLocaleVersion(locale, LocalePriority.REGION, title.titles)
-
-export const getLocalizedDescription = (title: Title, locale: Locale): TitleDescription | null =>
-    selectLocaleVersion(locale, LocalePriority.LOCALE, title.descriptions)
-
-export const getLocalizedRating = (title: Title, locale: Locale): Rating | null =>
-    selectRegionalVersion(locale, title.ratings)
-
-export const getPlayableMedia = (title: Title, playabilityRating: (media: Media) => PlayabilityRating): Media | null =>
-    selectPlayabileMedia(playabilityRating, title.media)
diff --git a/src/api/models/dto/ContentMeta.ts b/src/api/models/dto/ContentMeta.ts
new file mode 100644
index 0000000..cecb1f2
--- /dev/null
+++ b/src/api/models/dto/ContentMeta.ts
@@ -0,0 +1,7 @@
+import {Instalment} from "../Instalment";
+import {Content} from "../Content";
+
+export interface ContentMeta {
+    content: Content,
+    instalment: Instalment | null,
+}
diff --git a/src/api/models/dto/GenreWithContent.ts b/src/api/models/dto/GenreWithContent.ts
new file mode 100644
index 0000000..42adf74
--- /dev/null
+++ b/src/api/models/dto/GenreWithContent.ts
@@ -0,0 +1,7 @@
+import {Genre} from "../Genre";
+import {Content} from "../Content";
+
+export interface GenreWithContent {
+    genre: Genre,
+    content: Content[],
+}
diff --git a/src/api/models/dto/GenreWithTitles.ts b/src/api/models/dto/GenreWithTitles.ts
deleted file mode 100644
index 2d93e32..0000000
--- a/src/api/models/dto/GenreWithTitles.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {Genre} from "../Genre";
-import {Title} from "../Title";
-
-export interface GenreWithTitles {
-    genre: Genre,
-    titles: Title[],
-}
diff --git a/src/api/models/dto/TitleMeta.ts b/src/api/models/dto/TitleMeta.ts
deleted file mode 100644
index a6883fb..0000000
--- a/src/api/models/dto/TitleMeta.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {Episode} from "../Episode";
-import {Title} from "../Title";
-
-export interface TitleMeta {
-    title: Title,
-    show: Episode | null,
-}
diff --git a/src/routes/ContentRoute.tsx b/src/routes/ContentRoute.tsx
new file mode 100644
index 0000000..be5f46f
--- /dev/null
+++ b/src/routes/ContentRoute.tsx
@@ -0,0 +1,21 @@
+import React, {PropsWithChildren, useEffect, useState} from "react";
+import {useParams} from "react-router";
+import {useApiClient} from "../api/ApiClientContext";
+import {ContentMeta} from "../api/models/dto/ContentMeta";
+import {CurrentContentProvider} from "../util/CurrentContentContext";
+
+export function ContentRoute(props: PropsWithChildren<{}>) {
+    const {children} = props;
+    const {contentId} = useParams<{ contentId: string }>();
+    const {apiClient} = useApiClient();
+    const [meta, setMeta] = useState<ContentMeta | null>(null);
+    useEffect(() => {
+        apiClient.getContent(contentId).then(setMeta);
+    }, [apiClient, contentId, setMeta]);
+
+    return (
+        <CurrentContentProvider value={meta}>
+            {children}
+        </CurrentContentProvider>
+    )
+}
diff --git a/src/routes/TitleRoute.tsx b/src/routes/TitleRoute.tsx
deleted file mode 100644
index ccbaf91..0000000
--- a/src/routes/TitleRoute.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, {PropsWithChildren, useEffect, useState} from "react";
-import {useParams} from "react-router";
-import {useApiClient} from "../api/ApiClientContext";
-import {TitleMeta} from "../api/models/dto/TitleMeta";
-import {CurrentTitleProvider} from "../util/CurrentTitleContext";
-
-export function TitleRoute(props: PropsWithChildren<{}>) {
-    const {children} = props;
-    const {titleId} = useParams<{ titleId: string }>();
-    const {apiClient} = useApiClient();
-    const [meta, setMeta] = useState<TitleMeta | null>(null);
-    useEffect(() => {
-        apiClient.getTitle(titleId).then(setMeta);
-    }, [apiClient, titleId, setMeta]);
-
-    return (
-        <CurrentTitleProvider value={meta}>
-            {children}
-        </CurrentTitleProvider>
-    )
-}
diff --git a/src/routes/main/MainPage.tsx b/src/routes/main/MainPage.tsx
index 5b3d18a..3501ace 100644
--- a/src/routes/main/MainPage.tsx
+++ b/src/routes/main/MainPage.tsx
@@ -2,8 +2,8 @@ import React, {useEffect, useState} from "react";
 import {createUseStyles} from "react-jss";
 import {Link} from "react-router-dom";
 import {useApiClient} from "../../api/ApiClientContext";
-import {Episode} from "../../api/models/Episode";
-import {getLocalizedDescription, getLocalizedName, getLocalizedRating, Title} from "../../api/models/Title";
+import {Instalment} from "../../api/models/Instalment";
+import {getLocalizedDescription, getLocalizedName, getLocalizedRating, Content} from "../../api/models/Content";
 import {Locale} from "../../util/locale/Locale";
 import {sortLexicallyAsc} from "../../util/sort/sortLexically";
 
@@ -13,31 +13,31 @@ interface Props {
 
 export function MainPage(props: Props) {
     const {apiClient} = useApiClient();
-    const [data, setData] = useState<Title[]>();
+    const [data, setData] = useState<Content[]>();
     useEffect(() => {
-        apiClient.listTitles().then(setData);
+        apiClient.listContent().then(setData);
     }, [apiClient]);
     const locale: Locale = {
         language: "en",
         region: "DE",
     };
-    const [episodes, setEpisodes] = useState<{ [key: string]: Episode[] }>({});
-    const titlesToLoad = data?.filter(it => it.kind === "show" && episodes[it.ids.uuid] === undefined);
+    const [episodes, setEpisodes] = useState<{ [key: string]: Instalment[] }>({});
+    const contentToLoad = data?.filter(it => it.kind === "show" && episodes[it.ids.uuid] === undefined);
     useEffect(() => {
-        if (titlesToLoad?.length !== undefined && titlesToLoad.length > 0) {
-            const titlesBeingLoaded = Object.fromEntries(titlesToLoad?.map(it => [it.ids.uuid, []]) || []);
-            setEpisodes({...titlesBeingLoaded, ...episodes});
-            Promise.all(titlesToLoad?.map(it => apiClient.listEpisodes(it.ids.uuid).then(eps => [it.ids.uuid, eps])) || [])
+        if (contentToLoad?.length !== undefined && contentToLoad.length > 0) {
+            const contentBeingLoaded = Object.fromEntries(contentToLoad?.map(it => [it.ids.uuid, []]) || []);
+            setEpisodes({...contentBeingLoaded, ...episodes});
+            Promise.all(contentToLoad?.map(it => apiClient.listEpisodes(it.ids.uuid).then(eps => [it.ids.uuid, eps])) || [])
                 .then(it => setEpisodes({...Object.fromEntries(it), ...episodes}));
         }
-    }, [apiClient, episodes, titlesToLoad]);
+    }, [apiClient, episodes, contentToLoad]);
 
     const classes = useStyles();
 
     return (
         <div>
             {data?.sort(sortLexicallyAsc(item => getLocalizedName(item, locale)?.name || ""))
-                .map((item: Title) => {
+                .map((item: Content) => {
                     const title = getLocalizedName(item, locale);
                     const description = getLocalizedDescription(item, locale);
                     const rating = getLocalizedRating(item, locale);
@@ -58,8 +58,8 @@ export function MainPage(props: Props) {
                             {item.kind === "show" && (
                                 <ul>
                                     {episodes[item.ids.uuid]?.map(episode => {
-                                        const episodeTitle = getLocalizedName(episode.title, locale);
-                                        const episodeDescription = getLocalizedDescription(episode.title, locale);
+                                        const episodeTitle = getLocalizedName(episode.content, locale);
+                                        const episodeDescription = getLocalizedDescription(episode.content, locale);
                                         return (
                                             <li>
                                                 <p>
@@ -69,7 +69,7 @@ export function MainPage(props: Props) {
                                                 <p><strong>{episodeDescription?.tagline}</strong></p>
                                                 <p>{episodeDescription?.overview}</p>
                                                 <p>
-                                                    <Link to={"/player/" + episode.title.ids.uuid}>Play</Link>
+                                                    <Link to={"/player/" + episode.content.ids.uuid}>Play</Link>
                                                 </p>
                                             </li>
                                         )
diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx
index 59055a6..132716c 100644
--- a/src/routes/player/Player.tsx
+++ b/src/routes/player/Player.tsx
@@ -1,52 +1,68 @@
-import React, {useMemo, useState} from "react";
+import React, {useState} from "react";
 import {createUseStyles} from "react-jss";
 import {Link} from "react-router-dom";
-import {TitleMeta} from "../../api/models/dto/TitleMeta";
+import {ContentMeta} from "../../api/models/dto/ContentMeta";
 import {Media} from "../../api/models/Media";
-import {getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Title";
+import {getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Content";
 import {formatDuration} from "../../util/formatDuration";
 import {useLocale} from "../../util/locale/LocalizedContext";
 import {useDuration} from "../../util/media/useDuration";
 import {usePosition} from "../../util/media/usePosition";
 import {SeekBar} from "./SeekBar";
+import {VideoElement} from "./video/VideoElement";
+import {VideoApi} from "./video/VideoApi";
+import {useAudioTracks} from "../../util/media/useAudioTracks";
+import {useDebugInfo} from "../../util/media/useDebugInfo";
 
 interface Props {
-    title: TitleMeta,
+    meta: ContentMeta,
     media: Media,
 }
 
-export function Player(props: Props) {
-    const {media} = props;
-    const {title, show} = props.title;
-
+export function Player(
+    {meta, media}: Props
+) {
+    const {content, instalment} = meta;
     const classes = useStyles();
     const locale = useLocale();
 
-    const name = getLocalizedName(title, locale);
-    const description = getLocalizedDescription(title, locale);
-    const rating = getLocalizedRating(title, locale);
+    const name = getLocalizedName(content, locale);
+    const description = getLocalizedDescription(content, locale);
+    const rating = getLocalizedRating(content, locale);
 
     const [previewTrackElement, setPreviewTrackElement] = useState<HTMLTrackElement | null>(null);
-    const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
+    const [videoElement, setVideoElement] = useState<VideoApi | null>(null);
+    useDebugInfo("content", content);
+    useDebugInfo("player", videoElement);
 
-    const duration = useDuration(videoElement);
     const position = usePosition(videoElement);
-
-    const video = useMemo(() => (
-        <video ref={setVideoElement} src={media.src} className={classes.player} crossOrigin="anonymous" controls>
-            {title.preview && (
-                <track ref={setPreviewTrackElement} kind="metadata" label="previews" src={title.preview}/>
-            )}
-        </video>
-    ), [classes.player, media.src, title.preview]);
+    const duration = useDuration(videoElement);
+    const audioTracks = useAudioTracks(videoElement);
 
     return (
         <div>
             <Link to="/">Back</Link>
             <p>{name?.name}</p>
-            <p>{show && getLocalizedName(show.title, locale)?.name}</p>
-            {video}
+            <p>{instalment?.content && getLocalizedName(instalment?.content, locale)?.name}</p>
+            <VideoElement
+                media={media}
+                autoPlay={true}
+                ref={setVideoElement}
+                previewSrc={content.preview || undefined}
+                previewTrackRef={setPreviewTrackElement}
+            />
             <p style={{fontVariant: "tabular-nums"}}>{formatDuration(position)} / {formatDuration(duration)}</p>
+            <button onClick={videoElement?.play}>Play</button>
+            <button onClick={videoElement?.pause}>Pause</button>
+            <ul>
+                {audioTracks.map(track => (
+                    <li key={track.index}>
+                        <strong>{track.lang}</strong>
+                        {track.labels}
+                        <button onClick={() => videoElement?.setAudioTrack(track)}>Choose</button>
+                    </li>
+                ))}
+            </ul>
             <SeekBar
                 video={videoElement}
                 previewTrack={previewTrackElement}
diff --git a/src/routes/player/PlayerPage.tsx b/src/routes/player/PlayerPage.tsx
index b1f2aa8..eb96af0 100644
--- a/src/routes/player/PlayerPage.tsx
+++ b/src/routes/player/PlayerPage.tsx
@@ -1,23 +1,23 @@
 import React from "react";
-import {getPlayableMedia} from "../../api/models/Title";
+import {getPlayableMedia} from "../../api/models/Content";
 import {usePlayabilityRating} from "../../util/mime/usePlayabilityRating";
-import {useCurrentTitle} from "../../util/CurrentTitleContext";
+import {useCurrentContent} from "../../util/CurrentContentContext";
 import {Player} from "./Player";
 import {PlayerError} from "./PlayerError";
 import {PlayerLoading} from "./PlayerLoading";
 
 export function PlayerPage() {
-    const title = useCurrentTitle();
+    const content = useCurrentContent();
     const playabilityRating = usePlayabilityRating();
 
-    if (title === null) {
+    if (content === null) {
         return (<PlayerLoading/>);
     }
 
-    const media = getPlayableMedia(title.title, playabilityRating);
+    const media = getPlayableMedia(content.content, playabilityRating);
     if (media === null) {
         return (<PlayerError/>);
     }
 
-    return (<Player title={title} media={media}/>);
+    return (<Player meta={content} media={media}/>);
 }
diff --git a/src/routes/player/SeekBar.tsx b/src/routes/player/SeekBar.tsx
index b138c8e..90bcbda 100644
--- a/src/routes/player/SeekBar.tsx
+++ b/src/routes/player/SeekBar.tsx
@@ -5,10 +5,11 @@ import {MousePosition} from "../../util/mouse/MousePosition";
 import {useOffsetAbsolute} from "../../util/offset/useOffsetAbsolute";
 import {useOffsetRelative} from "../../util/offset/useOffsetRelative";
 import {PreviewBar} from "./PreviewBar";
+import {VideoApi} from "./video/VideoApi";
 
 interface Props {
     previewTrack: HTMLTrackElement | null,
-    video: HTMLVideoElement | null,
+    video: VideoApi | null,
     duration: number,
     position: number,
 }
@@ -34,7 +35,7 @@ export function SeekBar({video, previewTrack, duration, position}: Props) {
         const position = getMousePosition(event);
         setMousePosition(position);
         if (video && position) {
-            video.currentTime = position.relative * duration;
+            video.setCurrentTime(position.relative * duration);
         }
     }, [duration, video]);
 
diff --git a/src/routes/player/video/DashVideoElement.tsx b/src/routes/player/video/DashVideoElement.tsx
new file mode 100644
index 0000000..43cd84b
--- /dev/null
+++ b/src/routes/player/video/DashVideoElement.tsx
@@ -0,0 +1,129 @@
+import React, {useEffect, useImperativeHandle, useMemo, useState} from "react";
+import {createUseStyles} from "react-jss";
+import {Media} from "../../../api/models/Media";
+import dashjs, {MediaInfo} from "dashjs";
+import {VideoApi} from "./VideoApi";
+
+interface Props {
+    media: Media,
+    autoPlay: boolean,
+    previewSrc?: string,
+    previewTrackRef: React.Ref<HTMLTrackElement>
+}
+
+interface VideoQuality {
+
+}
+
+export const DashVideoElement = React.forwardRef<VideoApi, Props>(function (
+    {media, autoPlay, previewSrc, previewTrackRef},
+    ref
+) {
+    const classes = useStyles();
+
+    const player = useMemo(() => dashjs.MediaPlayer().create(), []);
+    const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
+
+    useImperativeHandle(ref, () => ({
+        METADATA_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED,
+        TIMECHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED,
+        play: () => {
+            player.play();
+        },
+        pause: () => {
+            player.pause();
+        },
+        isPaused: () => {
+            return player.isPaused();
+        },
+        setPlaybackRate: (value: number) => {
+            player.setPlaybackRate(value);
+        },
+        getPlaybackRate: () => {
+            return player.getPlaybackRate();
+        },
+        setCurrentTime(value: number) {
+            player.seek(value);
+        },
+        getCurrentTime(): number {
+            return player.time();
+        },
+        getDuration(): number {
+            return player.duration();
+        },
+        canPlay(): boolean {
+            return !Number.isNaN(player.duration());
+        },
+        setAudioTrack(track: MediaInfo) {
+            player.setQualityFor("audio", 0);
+            player.setCurrentTrack(track);
+        },
+        addEventListener(event: string, listener: () => void) {
+            player.on(event, listener);
+        },
+        removeEventListener(event: string, listener: () => void) {
+            player.off(event, listener);
+        },
+        getAudioTracks(): MediaInfo[] {
+            return player.getTracksFor("audio").map((info: MediaInfo) => {
+                return info;
+            })
+        },
+        debug(): any {
+            return player;
+        }
+    }), [player]);
+
+    useEffect(() => {
+        player.initialize();
+        player.setTrackSwitchModeFor("audio", "alwaysReplace");
+        return () => {
+            return player.reset();
+        }
+    }, [player]);
+
+    useEffect(() => {
+        if (videoElement) {
+            player.attachView(videoElement)
+        }
+        return () => {
+            // @ts-ignore
+            player.attachView(null);
+        }
+    }, [player, videoElement]);
+
+    useEffect(() => {
+        player.attachSource(media.src);
+        return () => {
+            // @ts-ignore
+            player.attachSource(null);
+        }
+    }, [player, media]);
+
+    useEffect(() => {
+        player.setAutoPlay(autoPlay);
+    }, [player, autoPlay]);
+
+    return (
+        <video
+            ref={setVideoElement}
+            className={classes.player}
+            crossOrigin="anonymous"
+            controls
+        >
+            {previewSrc && (
+                <track ref={previewTrackRef} kind="metadata" label="previews" src={previewSrc}/>
+            )}
+        </video>
+    );
+});
+
+const useStyles = createUseStyles({
+    player: {
+        maxWidth: "40rem",
+        margin: {
+            left: "auto",
+            right: "auto",
+        }
+    }
+});
diff --git a/src/routes/player/video/RawVideoElement.tsx b/src/routes/player/video/RawVideoElement.tsx
new file mode 100644
index 0000000..13a5448
--- /dev/null
+++ b/src/routes/player/video/RawVideoElement.tsx
@@ -0,0 +1,129 @@
+import React, {useImperativeHandle, useState} from "react";
+import {createUseStyles} from "react-jss";
+import {Media} from "../../../api/models/Media";
+import {VideoApi} from "./VideoApi";
+import {MediaInfo} from "dashjs";
+
+interface Props {
+    media: Media,
+    autoPlay: boolean,
+    previewSrc?: string,
+    previewTrackRef: React.Ref<HTMLTrackElement>
+}
+
+export const RawVideoElement = React.forwardRef<VideoApi, Props>(function (
+    {media, autoPlay, previewSrc, previewTrackRef},
+    ref
+) {
+    const classes = useStyles();
+    const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
+
+    useImperativeHandle(ref, () => ({
+        METADATA_EVENT: "loadedmetadata",
+        TIMECHANGE_EVENT: "timeupdate",
+        play() {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.play();
+        },
+        pause() {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.pause();
+        },
+        isPaused(): boolean {
+            if (!videoElement) {
+                return true;
+            }
+
+            return videoElement.paused;
+        },
+        setPlaybackRate(value: number) {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.playbackRate = value;
+        },
+        getPlaybackRate(): number {
+            if (!videoElement) {
+                return 1;
+            }
+
+            return videoElement.playbackRate;
+        },
+        setCurrentTime(value: number) {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.currentTime = value;
+        },
+        getCurrentTime(): number {
+            if (!videoElement) {
+                return 0;
+            }
+
+            return videoElement.currentTime;
+        },
+        getDuration(): number {
+            if (!videoElement) {
+                return 0;
+            }
+
+            return videoElement.duration;
+        },
+        canPlay(): boolean {
+            if (!videoElement) {
+                return false;
+            }
+
+            return videoElement.readyState >= 1;
+        },
+        getAudioTracks(): MediaInfo[] {
+            return [];
+        },
+        setAudioTrack(track: MediaInfo) {
+        },
+        addEventListener(event: string, listener: () => void) {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.addEventListener(event, listener);
+        },
+        removeEventListener(event: string, listener: () => void) {
+            if (!videoElement) {
+                return;
+            }
+
+            videoElement.removeEventListener(event, listener);
+        },
+        debug(): any {
+            return videoElement;
+        }
+    }), [videoElement]);
+
+    return (
+        <video ref={setVideoElement} autoPlay={autoPlay} src={media.src} className={classes.player}
+               crossOrigin="anonymous" controls>
+            {previewSrc && (
+                <track ref={previewTrackRef} kind="metadata" label="previews" src={previewSrc}/>
+            )}
+        </video>
+    );
+});
+
+const useStyles = createUseStyles({
+    player: {
+        maxWidth: "40rem",
+        margin: {
+            left: "auto",
+            right: "auto",
+        }
+    }
+});
diff --git a/src/routes/player/video/VideoApi.ts b/src/routes/player/video/VideoApi.ts
new file mode 100644
index 0000000..fd53529
--- /dev/null
+++ b/src/routes/player/video/VideoApi.ts
@@ -0,0 +1,35 @@
+import {MediaInfo} from "dashjs";
+
+export interface VideoApi {
+    METADATA_EVENT: string
+    TIMECHANGE_EVENT: string
+
+    play(): void
+
+    pause(): void
+
+    isPaused(): boolean
+
+    setPlaybackRate(value: number): void
+
+    getPlaybackRate(): number
+
+    setCurrentTime(value: number): void
+
+    getCurrentTime(): number
+
+    getDuration(): number
+
+    canPlay(): boolean
+
+    getAudioTracks(): MediaInfo[]
+
+    setAudioTrack(track: MediaInfo): void
+
+    addEventListener(event: string, listener: () => void): void
+
+    removeEventListener(event: string, listener: () => void): void
+
+    debug(): any
+
+}
diff --git a/src/routes/player/video/VideoElement.tsx b/src/routes/player/video/VideoElement.tsx
new file mode 100644
index 0000000..cb2eff0
--- /dev/null
+++ b/src/routes/player/video/VideoElement.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import {Media} from "../../../api/models/Media";
+import {DashVideoElement} from "./DashVideoElement";
+import {RawVideoElement} from "./RawVideoElement";
+import {VideoApi} from "./VideoApi";
+
+interface Props {
+    media: Media,
+    autoPlay: boolean,
+    previewSrc?: string,
+    previewTrackRef: React.Ref<HTMLTrackElement>
+}
+
+export const VideoElement = React.forwardRef<VideoApi, Props>(function (
+    props: Props, ref
+) {
+    switch (props.media.mime) {
+        case "application/dash+xml":
+            return <DashVideoElement ref={ref} {...props} />;
+        default:
+            return <RawVideoElement ref={ref} {...props} />;
+    }
+});
diff --git a/src/util/CurrentContentContext.ts b/src/util/CurrentContentContext.ts
new file mode 100644
index 0000000..6b39801
--- /dev/null
+++ b/src/util/CurrentContentContext.ts
@@ -0,0 +1,7 @@
+import {createContext, useContext} from "react";
+import {ContentMeta} from "../api/models/dto/ContentMeta";
+
+const CurrentContentContext = createContext<ContentMeta | null>(null);
+export const CurrentContentProvider = CurrentContentContext.Provider;
+export const CurrentContentConsumer = CurrentContentContext.Consumer;
+export const useCurrentContent = () => useContext(CurrentContentContext);
diff --git a/src/util/CurrentTitleContext.ts b/src/util/CurrentTitleContext.ts
deleted file mode 100644
index 3576580..0000000
--- a/src/util/CurrentTitleContext.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {createContext, useContext} from "react";
-import {TitleMeta} from "../api/models/dto/TitleMeta";
-
-const CurrentTitleContext = createContext<TitleMeta | null>(null);
-export const CurrentTitleProvider = CurrentTitleContext.Provider;
-export const CurrentTitleConsumer = CurrentTitleContext.Consumer;
-export const useCurrentTitle = () => useContext(CurrentTitleContext);
diff --git a/src/util/media/useAudioTracks.ts b/src/util/media/useAudioTracks.ts
new file mode 100644
index 0000000..5076217
--- /dev/null
+++ b/src/util/media/useAudioTracks.ts
@@ -0,0 +1,25 @@
+import {useEffect, useState} from "react";
+import {VideoApi} from "../../routes/player/video/VideoApi";
+import {MediaInfo} from "dashjs";
+
+export const useAudioTracks = (video: VideoApi | null) => {
+    const [audioTracks, setAudioTracks] = useState<MediaInfo[]>([]);
+    useEffect(() => {
+        if (video !== null) {
+            if (video.canPlay()) {
+                setAudioTracks(video.getAudioTracks());
+            } else {
+                const listener = () => {
+                    window.requestAnimationFrame(() => {
+                        setAudioTracks(video.getAudioTracks());
+                    })
+                };
+                video.addEventListener(video.METADATA_EVENT, listener)
+                return () => {
+                    video.removeEventListener(video.METADATA_EVENT, listener)
+                }
+            }
+        }
+    }, [video]);
+    return audioTracks;
+}
diff --git a/src/util/media/useDebugInfo.ts b/src/util/media/useDebugInfo.ts
new file mode 100644
index 0000000..c450f4b
--- /dev/null
+++ b/src/util/media/useDebugInfo.ts
@@ -0,0 +1,22 @@
+import {useEffect} from "react";
+
+function onlyInDebug<U, R>(callback: (...args: U[]) => R, defValue?: R): (...args: U[]) => R {
+    if (process.env.NODE_ENV === "development") {
+        return callback
+    } else {
+        return () => defValue as R;
+    }
+}
+
+export const useDebugInfo = onlyInDebug((key: string, value: any) => {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    useEffect(() => {
+        // @ts-ignore
+        if (!window.mediaDebug) {
+            // @ts-ignore
+            window.mediaDebug = {};
+        }
+        // @ts-ignore
+        window.mediaDebug[key] = value;
+    }, [key, value]);
+});
diff --git a/src/util/media/useDuration.ts b/src/util/media/useDuration.ts
index 42cdb09..8b2d42b 100644
--- a/src/util/media/useDuration.ts
+++ b/src/util/media/useDuration.ts
@@ -1,20 +1,21 @@
 import {useEffect, useState} from "react";
+import {VideoApi} from "../../routes/player/video/VideoApi";
 
-export const useDuration = (video: HTMLVideoElement | null) => {
+export const useDuration = (video: VideoApi | null) => {
     const [duration, setDuration] = useState<number>(0);
     useEffect(() => {
         if (video !== null) {
-            if (video.readyState >= 1) {
-                setDuration(video.duration);
+            if (video.canPlay()) {
+                setDuration(video.getDuration());
             } else {
                 const listener = () => {
                     window.requestAnimationFrame(() => {
-                        setDuration(video.duration);
+                        setDuration(video.getDuration());
                     })
                 };
-                video.addEventListener("loadedmetadata", listener)
+                video.addEventListener(video.METADATA_EVENT, listener)
                 return () => {
-                    video.removeEventListener("loadedmetadata", listener)
+                    video.removeEventListener(video.METADATA_EVENT, listener)
                 }
             }
         }
diff --git a/src/util/media/usePosition.ts b/src/util/media/usePosition.ts
index fb1b390..28db74d 100644
--- a/src/util/media/usePosition.ts
+++ b/src/util/media/usePosition.ts
@@ -1,17 +1,18 @@
 import {useEffect, useState} from "react";
+import {VideoApi} from "../../routes/player/video/VideoApi";
 
-export const usePosition = (video: HTMLVideoElement | null) => {
+export const usePosition = (video: VideoApi | null) => {
     const [position, setPosition] = useState<number>(0);
     useEffect(() => {
         if (video !== null) {
             const listener = () => {
                 window.requestAnimationFrame(() => {
-                    setPosition(video.currentTime);
+                    setPosition(video.getCurrentTime());
                 })
             };
-            video.addEventListener("timeupdate", listener)
+            video.addEventListener(video.TIMECHANGE_EVENT, listener)
             return () => {
-                video.removeEventListener("timeupdate", listener)
+                video.removeEventListener(video.TIMECHANGE_EVENT, listener)
             }
         }
     }, [video]);
-- 
GitLab