diff --git a/.gitea/workflows/check-js.yaml b/.gitea/workflows/check-js.yaml
new file mode 100644
index 00000000..44563f46
--- /dev/null
+++ b/.gitea/workflows/check-js.yaml
@@ -0,0 +1,38 @@
+name: Check JS
+
+on:
+ pull_request:
+ push:
+ branches:
+ - railbird-v6
+
+jobs:
+ typescript:
+ name: Check TS (tsc)
+ runs-on: nixos-x86_64-linux
+ defaults:
+ run:
+ shell: nix shell nixpkgs#nodejs_22 nixpkgs#yarn -c bash -e {0}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install node_modules and build package
+ run: yarn install --immutable
+ - name: Check TypeScript
+ run: yarn tsc
+ - name: Test web HLS
+ run: yarn test:web
+
+ lint:
+ name: Lint JS (eslint, prettier)
+ runs-on: nixos-x86_64-linux
+ defaults:
+ run:
+ shell: nix shell nixpkgs#nodejs_22 nixpkgs#yarn -c bash -e {0}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install node_modules and build package
+ run: yarn install --immutable
+ - name: Run ESLint
+ run: yarn lint
+ - name: Verify auto-fix produces no changes
+ run: yarn lint --fix && git diff --exit-code HEAD
diff --git a/package.json b/package.json
index 9a153f1a..792f4890 100644
--- a/package.json
+++ b/package.json
@@ -31,11 +31,10 @@
"react": "18.2.0",
"react-native": "0.73.2",
"react-native-windows": "^0.61.0-0",
- "release-it": "^16.2.1",
- "patch-package": "^8.0.0"
+ "release-it": "^16.2.1"
},
"dependencies": {
- "shaka-player": "^4.11.7",
+ "hls.js": "^1.6.16",
"typescript": "5.6.3"
},
"peerDependencies": {
@@ -51,6 +50,7 @@
"docs": "yarn --cwd docs build",
"release": "release-it",
"test": "echo no test available",
+ "test:web": "yarn tsc -p test/tsconfig.json && node --test test/web-hls.test.js",
"check-ios": "scripts/swift-format.sh && scripts/swift-lint.sh && scripts/clang-format.sh",
"check-android": "scripts/kotlin-lint.sh",
"check-all": "yarn check-android; yarn check-ios; yarn lint",
diff --git a/patches/shaka-player+4.11.7.patch b/patches/shaka-player+4.11.7.patch
deleted file mode 100644
index ee5b340f..00000000
--- a/patches/shaka-player+4.11.7.patch
+++ /dev/null
@@ -1,39 +0,0 @@
-diff --git a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
-index 19c0930..cc0a3fd 100644
---- a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
-+++ b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
-@@ -5117,3 +5117,5 @@ declare namespace shaka.extern {
- declare namespace shaka.extern {
- type TransmuxerPlugin = ( ) => shaka.extern.Transmuxer ;
- }
-+
-+export default shaka;
-diff --git a/node_modules/shaka-player/dist/shaka-player.ui.d.ts b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
-index 1618ca0..a6076c6 100644
---- a/node_modules/shaka-player/dist/shaka-player.ui.d.ts
-+++ b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
-@@ -5830,3 +5830,5 @@ declare namespace shaka.extern {
- declare namespace shaka.extern {
- type UIVolumeBarColors = { base : string , level : string } ;
- }
-+
-+export default shaka;
-diff --git a/node_modules/shaka-player/index.d.ts b/node_modules/shaka-player/index.d.ts
-new file mode 100644
-index 0000000..3ebfd96
---- /dev/null
-+++ b/node_modules/shaka-player/index.d.ts
-@@ -0,0 +1,2 @@
-+///
-+///
-\ No newline at end of file
-diff --git a/node_modules/shaka-player/ui.d.ts b/node_modules/shaka-player/ui.d.ts
-new file mode 100644
-index 0000000..84a3be0
---- /dev/null
-+++ b/node_modules/shaka-player/ui.d.ts
-@@ -0,0 +1,3 @@
-+import shaka from 'shaka-player/dist/shaka-player.ui'
-+export * from 'shaka-player/dist/shaka-player.ui'
-+export default shaka;
-\ No newline at end of file
diff --git a/src/Video.tsx b/src/Video.tsx
index b9eb6102..42df8a36 100644
--- a/src/Video.tsx
+++ b/src/Video.tsx
@@ -475,7 +475,7 @@ const Video = forwardRef(
(e: NativeSyntheticEvent) => {
onSeekComplete?.(e.nativeEvent);
},
- [onSeekComplete]
+ [onSeekComplete],
);
const onVideoPlaybackStateChanged = useCallback(
@@ -733,10 +733,10 @@ const Video = forwardRef(
}
// poster style
- const baseStyle: StyleProp = {
- ...StyleSheet.absoluteFill,
- resizeMode: _posterResizeMode,
- };
+ const baseStyle: StyleProp = [
+ StyleSheet.absoluteFill,
+ {resizeMode: _posterResizeMode},
+ ];
let posterStyle: StyleProp = baseStyle;
@@ -786,10 +786,10 @@ const Video = forwardRef(
]);
const _style: StyleProp = useMemo(
- () => ({
- ...StyleSheet.absoluteFill,
- ...(showPoster ? {display: 'none'} : {}),
- }),
+ () => [
+ StyleSheet.absoluteFill,
+ showPoster ? {display: 'none'} : undefined,
+ ],
[showPoster],
);
diff --git a/src/Video.web.tsx b/src/Video.web.tsx
index 7bcfcb7b..1fccca7e 100644
--- a/src/Video.web.tsx
+++ b/src/Video.web.tsx
@@ -4,70 +4,32 @@ import React, {
useEffect,
useImperativeHandle,
useRef,
- useState,
type RefObject,
} from 'react';
-//@ts-ignore
-import shaka from 'shaka-player';
+import Hls from 'hls.js';
import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
+import {
+ createWebPlaybackErrorReporter,
+ describeHlsError,
+ describeMediaError,
+ isAppleWebKit,
+ selectWebHlsPlaybackMode,
+ type WebHlsPlaybackMode,
+} from './webHls';
-// Action Queue Class
-class ActionQueue {
- private queue: { action: () => Promise; name: string }[] = [];
- private isRunning = false;
+const HLS_MIME = 'application/vnd.apple.mpegurl';
- enqueue(action: () => Promise, name: string) {
- this.queue.push({ action, name });
- this.runNext();
+// `poster` is either a (deprecated) plain URI string or a poster object whose
+// `source` follows the RN image-source shape. The DOM only takes a URL string.
+const resolvePosterUri = (
+ poster: ReactVideoProps['poster'],
+): string | undefined => {
+ if (typeof poster === 'string') {
+ return poster;
}
-
- private async runNext() {
- if (this.isRunning || this.queue.length === 0) {
- console.log("Refusing to run in runNext", this.queue.length, this.isRunning);
- return;
- }
- this.isRunning = true;
- const { action, name } = this.queue.shift()!;
- console.log(`Running action: ${name}`);
-
- const actionPromise = action();
- const timeoutPromise = new Promise((_, reject) =>
- setTimeout(() => reject(new Error(`Action ${name} timed out`)), 2000)
- );
-
- try {
- await Promise.race([actionPromise, timeoutPromise]);
- } catch (e) {
- console.error('Error in queued action:', e);
- } finally {
- this.isRunning = false;
- this.runNext();
- }
- }
-}
-
-function shallowEqual(obj1: any, obj2: any) {
- // If both are strictly equal (covers primitive types and identical object references)
- if (obj1 === obj2) return true;
-
- // If one is not an object (meaning it's a primitive), they must be strictly equal
- if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
- return false;
- }
-
- // Get the keys of both objects
- const keys1 = Object.keys(obj1);
- const keys2 = Object.keys(obj2);
-
- // If the number of keys is different, the objects are not equal
- if (keys1.length !== keys2.length) return false;
-
- // Check that all keys and their corresponding values are the same
- return keys1.every(key => {
- // If the value is an object, we fall back to reference equality (shallow comparison)
- return obj1[key] === obj2[key];
- });
-}
+ const source = poster?.source;
+ return typeof source === 'object' && source !== null ? source.uri : undefined;
+};
const Video = forwardRef(
(
@@ -99,62 +61,46 @@ const Video = forwardRef(
ref,
) => {
const nativeRef = useRef(null);
- const shakaPlayerRef = useRef(null);
- const [currentSource, setCurrentSource] = useState