@symbiote-native/video 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -2
- package/build/angular/index.d.ts +4 -0
- package/build/angular/index.js +5 -0
- package/build/angular/inject-video-player.d.ts +4 -0
- package/build/angular/inject-video-player.js +6 -0
- package/build/angular/video-airplay-button/index.d.ts +12 -0
- package/build/angular/video-airplay-button/index.js +78 -0
- package/build/angular/video-view/index.d.ts +17 -0
- package/build/angular/video-view/index.js +97 -0
- package/build/core/constants.d.ts +12 -0
- package/build/core/constants.js +13 -0
- package/build/core/index.d.ts +10 -0
- package/build/core/index.js +8 -0
- package/build/core/native-module.d.ts +10 -0
- package/build/core/native-module.js +3 -0
- package/build/core/player-types.d.ts +221 -0
- package/build/core/player-types.js +1 -0
- package/build/core/video-airplay-button.d.ts +4 -0
- package/build/core/video-airplay-button.js +29 -0
- package/build/core/video-module.d.ts +12 -0
- package/build/core/video-module.js +21 -0
- package/build/core/video-player-controller.d.ts +15 -0
- package/build/core/video-player-controller.js +21 -0
- package/build/core/video-player.d.ts +7 -0
- package/build/core/video-player.js +22 -0
- package/build/core/video-source.d.ts +3 -0
- package/build/core/video-source.js +20 -0
- package/build/core/video-thumbnail.d.ts +4 -0
- package/build/core/video-thumbnail.js +3 -0
- package/build/core/video-view.d.ts +18 -0
- package/build/core/video-view.js +63 -0
- package/build/core/view-types.d.ts +90 -0
- package/build/core/view-types.js +1 -0
- package/build/react/index.d.ts +4 -0
- package/build/react/index.js +4 -0
- package/build/react/use-video-player.d.ts +7 -0
- package/build/react/use-video-player.js +11 -0
- package/build/react/video-view.d.ts +13 -0
- package/build/react/video-view.js +10 -0
- package/build/solid/index.d.ts +3 -0
- package/build/solid/index.js +4 -0
- package/build/solid/use-video-player.d.ts +8 -0
- package/build/solid/use-video-player.js +11 -0
- package/build/solid/video-view.d.ts +33 -0
- package/build/solid/video-view.js +6 -0
- package/build/svelte/index.d.ts +5 -0
- package/build/svelte/index.js +5 -0
- package/build/svelte/use-video-player.svelte.d.ts +10 -0
- package/build/svelte/use-video-player.svelte.js +15 -0
- package/build/svelte/video-airplay-button.svelte +12 -0
- package/build/svelte/video-airplay-button.svelte.d.ts +4 -0
- package/build/svelte/video-view.svelte +25 -0
- package/build/svelte/video-view.svelte.d.ts +9 -0
- package/build/vue/index.d.ts +3 -0
- package/build/vue/index.js +4 -0
- package/build/vue/use-video-player.d.ts +8 -0
- package/build/vue/use-video-player.js +16 -0
- package/build/vue/video-view.d.ts +5 -0
- package/build/vue/video-view.js +14 -0
- package/build-ngc/angular/index.d.ts +4 -0
- package/build-ngc/angular/index.js +6 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/inject-video-player.d.ts +4 -0
- package/build-ngc/angular/inject-video-player.js +7 -0
- package/build-ngc/angular/inject-video-player.js.map +1 -0
- package/build-ngc/angular/video-airplay-button/index.d.ts +15 -0
- package/build-ngc/angular/video-airplay-button/index.js +37 -0
- package/build-ngc/angular/video-airplay-button/index.js.map +1 -0
- package/build-ngc/angular/video-view/index.d.ts +20 -0
- package/build-ngc/angular/video-view/index.js +56 -0
- package/build-ngc/angular/video-view/index.js.map +1 -0
- package/build-ngc/core/constants.d.ts +12 -0
- package/build-ngc/core/constants.js +14 -0
- package/build-ngc/core/constants.js.map +1 -0
- package/build-ngc/core/index.d.ts +10 -0
- package/build-ngc/core/index.js +9 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +10 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/player-types.d.ts +221 -0
- package/build-ngc/core/player-types.js +2 -0
- package/build-ngc/core/player-types.js.map +1 -0
- package/build-ngc/core/video-airplay-button.d.ts +4 -0
- package/build-ngc/core/video-airplay-button.js +30 -0
- package/build-ngc/core/video-airplay-button.js.map +1 -0
- package/build-ngc/core/video-module.d.ts +12 -0
- package/build-ngc/core/video-module.js +22 -0
- package/build-ngc/core/video-module.js.map +1 -0
- package/build-ngc/core/video-player-controller.d.ts +15 -0
- package/build-ngc/core/video-player-controller.js +22 -0
- package/build-ngc/core/video-player-controller.js.map +1 -0
- package/build-ngc/core/video-player.d.ts +7 -0
- package/build-ngc/core/video-player.js +23 -0
- package/build-ngc/core/video-player.js.map +1 -0
- package/build-ngc/core/video-source.d.ts +3 -0
- package/build-ngc/core/video-source.js +21 -0
- package/build-ngc/core/video-source.js.map +1 -0
- package/build-ngc/core/video-thumbnail.d.ts +4 -0
- package/build-ngc/core/video-thumbnail.js +4 -0
- package/build-ngc/core/video-thumbnail.js.map +1 -0
- package/build-ngc/core/video-view.d.ts +18 -0
- package/build-ngc/core/video-view.js +64 -0
- package/build-ngc/core/video-view.js.map +1 -0
- package/build-ngc/core/view-types.d.ts +90 -0
- package/build-ngc/core/view-types.js +2 -0
- package/build-ngc/core/view-types.js.map +1 -0
- package/native-link.json +33 -0
- package/package.json +144 -3
- package/src/angular/index.ts +6 -0
- package/src/angular/inject-video-player.ts +20 -0
- package/src/angular/video-airplay-button/index.ts +38 -0
- package/src/angular/video-view/index.ts +59 -0
- package/src/core/constants.ts +16 -0
- package/src/core/index.ts +56 -0
- package/src/core/native-module.ts +15 -0
- package/src/core/player-types.ts +241 -0
- package/src/core/video-airplay-button.ts +45 -0
- package/src/core/video-module.ts +25 -0
- package/src/core/video-player-controller.ts +53 -0
- package/src/core/video-player.ts +38 -0
- package/src/core/video-source.ts +21 -0
- package/src/core/video-thumbnail.ts +7 -0
- package/src/core/video-view.ts +119 -0
- package/src/core/view-types.ts +114 -0
- package/src/react/index.ts +6 -0
- package/src/react/use-video-player.ts +22 -0
- package/src/react/video-view.ts +35 -0
- package/src/solid/index.ts +5 -0
- package/src/solid/use-video-player.ts +24 -0
- package/src/solid/video-view.ts +22 -0
- package/src/svelte/index.ts +7 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +6 -0
- package/src/svelte/use-video-player.svelte.ts +31 -0
- package/src/svelte/video-airplay-button.svelte +12 -0
- package/src/svelte/video-view.svelte +25 -0
- package/src/vue/index.ts +5 -0
- package/src/vue/use-video-player.ts +33 -0
- package/src/vue/video-view.ts +24 -0
package/native-link.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"android": {
|
|
3
|
+
"gradleProjectName": "expo-video",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"importPath": "expo.modules.video.VideoModule",
|
|
7
|
+
"className": "VideoModule",
|
|
8
|
+
"nativeName": "ExpoVideo"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"optionalManifestBundles": [
|
|
12
|
+
{
|
|
13
|
+
"id": "backgroundPlayback",
|
|
14
|
+
"label": "Background video playback",
|
|
15
|
+
"warning": "Requesting FOREGROUND_SERVICE_MEDIA_PLAYBACK triggers Play Console policy review for a foreground service, Google asks for a justification before you can publish.",
|
|
16
|
+
"nextSteps": "Set `staysActiveInBackground` and `showNowPlayingNotification` on the player. On iOS add `audio` to UIBackgroundModes in Info.plist yourself.",
|
|
17
|
+
"manifestPermissions": [
|
|
18
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
19
|
+
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
|
|
20
|
+
],
|
|
21
|
+
"manifestServices": [
|
|
22
|
+
{
|
|
23
|
+
"name": "expo.modules.video.playbackService.ExpoVideoPlaybackService",
|
|
24
|
+
"foregroundServiceType": "mediaPlayback",
|
|
25
|
+
"intentFilterActions": [
|
|
26
|
+
"androidx.media3.session.MediaSessionService"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,147 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/video",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "expo-video wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Video playback: the player, the view, thumbnails, the cache and the AirPlay button.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
9
|
+
"directory": "packages/video"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/video#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"symbiote-native",
|
|
19
|
+
"expo-video",
|
|
20
|
+
"react",
|
|
21
|
+
"vue",
|
|
22
|
+
"svelte",
|
|
23
|
+
"solid",
|
|
24
|
+
"angular",
|
|
25
|
+
"video"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./build/core/index.js",
|
|
29
|
+
"module": "./build/core/index.js",
|
|
30
|
+
"types": "./build/core/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./build/core/index.d.ts",
|
|
34
|
+
"default": "./build/core/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./vue": {
|
|
37
|
+
"types": "./build/vue/index.d.ts",
|
|
38
|
+
"default": "./build/vue/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./react": {
|
|
41
|
+
"types": "./build/react/index.d.ts",
|
|
42
|
+
"default": "./build/react/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./svelte": "./src/svelte/index.ts",
|
|
45
|
+
"./solid": {
|
|
46
|
+
"types": "./build/solid/index.d.ts",
|
|
47
|
+
"default": "./build/solid/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./angular": {
|
|
50
|
+
"types": "./build-ngc/angular/index.d.ts",
|
|
51
|
+
"react-native": "./build-ngc/angular/index.js",
|
|
52
|
+
"default": "./src/angular/index.ts"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"src",
|
|
57
|
+
"build",
|
|
58
|
+
"build-ngc",
|
|
59
|
+
"native-link.json",
|
|
60
|
+
"!src/**/*.test.*",
|
|
61
|
+
"!src/**/*.spec.*",
|
|
62
|
+
"!src/**/*.detox.*"
|
|
63
|
+
],
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"expo-video": "57.0.5",
|
|
69
|
+
"expo-modules-core": "57.0.20",
|
|
70
|
+
"@symbiote-native/components": "3.2.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@angular/core": ">=20",
|
|
74
|
+
"@vue/runtime-core": "^3.5.13",
|
|
75
|
+
"react": ">=19.0.0",
|
|
76
|
+
"react-native": ">=0.86",
|
|
77
|
+
"solid-js": ">=1.9.0",
|
|
78
|
+
"svelte": ">=5.56.0",
|
|
79
|
+
"vue": ">=3.5.0",
|
|
80
|
+
"@symbiote-native/angular": "^3.3.0",
|
|
81
|
+
"@symbiote-native/engine": "^1.6.0",
|
|
82
|
+
"@symbiote-native/react": "^3.3.0",
|
|
83
|
+
"@symbiote-native/vue": "^3.3.0",
|
|
84
|
+
"@symbiote-native/svelte": "^3.2.0",
|
|
85
|
+
"@symbiote-native/solid": "^3.2.0"
|
|
86
|
+
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"@symbiote-native/angular": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@symbiote-native/react": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@symbiote-native/solid": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@symbiote-native/svelte": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@symbiote-native/vue": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"@angular/core": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@vue/runtime-core": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"react": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"solid-js": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"svelte": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"vue": {
|
|
119
|
+
"optional": true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"devDependencies": {
|
|
123
|
+
"@angular/compiler": "~22.0.8",
|
|
124
|
+
"@angular/compiler-cli": "~22.0.8",
|
|
125
|
+
"@angular/core": "~22.0.8",
|
|
126
|
+
"@types/node": "^26.0.0",
|
|
127
|
+
"@types/react": "^19.2.0",
|
|
128
|
+
"@vue/runtime-core": "^3.5.13",
|
|
129
|
+
"react": "19.2.3",
|
|
130
|
+
"solid-js": "^1.9.14",
|
|
131
|
+
"svelte": "^5.56.0",
|
|
132
|
+
"typescript": "~6.0.0",
|
|
133
|
+
"@symbiote-native/angular": "3.3.0",
|
|
134
|
+
"@symbiote-native/engine": "1.6.0",
|
|
135
|
+
"@symbiote-native/react": "3.3.0",
|
|
136
|
+
"@symbiote-native/solid": "3.2.0",
|
|
137
|
+
"@symbiote-native/svelte": "3.2.0",
|
|
138
|
+
"@symbiote-native/vue": "3.3.0",
|
|
139
|
+
"@symbiote-native/test-utils": "0.4.7"
|
|
140
|
+
},
|
|
141
|
+
"scripts": {
|
|
142
|
+
"typecheck": "tsc --build",
|
|
143
|
+
"clean": "rm -rf build-ngc",
|
|
144
|
+
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
145
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
146
|
+
}
|
|
6
147
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @symbiote-native/video/angular: плеер, view, миниатюры, кэш и кнопка AirPlay на общем ядре
|
|
2
|
+
|
|
3
|
+
export { VideoView } from './video-view';
|
|
4
|
+
export { VideoAirPlayButton } from './video-airplay-button';
|
|
5
|
+
export { injectVideoPlayer } from './inject-video-player';
|
|
6
|
+
export * from '../core';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Angular twin of `useVideoPlayer`, `injectX` shape matching `injectAudioPlayer`
|
|
2
|
+
|
|
3
|
+
import type { Signal } from '@angular/core';
|
|
4
|
+
import { createResourceHook } from '@symbiote-native/angular';
|
|
5
|
+
import { createVideoPlayerController } from '../core/video-player-controller';
|
|
6
|
+
import type {
|
|
7
|
+
IVideoPlayerBuilderOptions,
|
|
8
|
+
IVideoSource,
|
|
9
|
+
VideoPlayer,
|
|
10
|
+
} from '../core';
|
|
11
|
+
|
|
12
|
+
const injectPlayerResource = createResourceHook(createVideoPlayerController);
|
|
13
|
+
|
|
14
|
+
/** Released with the injection context, `setup` runs once on each new player */
|
|
15
|
+
export const injectVideoPlayer = (
|
|
16
|
+
source: () => IVideoSource,
|
|
17
|
+
setup?: (player: VideoPlayer) => void,
|
|
18
|
+
options?: () => IVideoPlayerBuilderOptions | undefined,
|
|
19
|
+
): Signal<VideoPlayer> =>
|
|
20
|
+
injectPlayerResource(() => [source(), options?.(), setup]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { DescriptorOutlet, NativeViewBase } from '@symbiote-native/angular';
|
|
3
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
4
|
+
import { renderVideoAirPlayButton } from '../../core';
|
|
5
|
+
import type { IVideoAirPlayButtonProps } from '../../core';
|
|
6
|
+
|
|
7
|
+
// One list is the inputs of the component and the props it hands to the native view
|
|
8
|
+
const AIRPLAY_INPUTS = [
|
|
9
|
+
'tint',
|
|
10
|
+
'activeTint',
|
|
11
|
+
'prioritizeVideoDevices',
|
|
12
|
+
'onBeginPresentingRoutes',
|
|
13
|
+
'onEndPresentingRoutes',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
// Types the inputs for the template, the decorators that would do it are replaced by `inputs`
|
|
17
|
+
export interface VideoAirPlayButton extends Partial<
|
|
18
|
+
Pick<IVideoAirPlayButtonProps, (typeof AIRPLAY_INPUTS)[number]>
|
|
19
|
+
> {}
|
|
20
|
+
|
|
21
|
+
/** Angular twin of `expo-video`'s `VideoAirPlayButton`, a plain view off iOS */
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'VideoAirPlayButton',
|
|
24
|
+
standalone: true,
|
|
25
|
+
imports: [DescriptorOutlet],
|
|
26
|
+
inputs: [...AIRPLAY_INPUTS],
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
template: `@if (descriptor; as node) {
|
|
29
|
+
<symbiote-descriptor-outlet [node]="node" />
|
|
30
|
+
}`,
|
|
31
|
+
})
|
|
32
|
+
export class VideoAirPlayButton extends NativeViewBase {
|
|
33
|
+
protected override readonly propNames = AIRPLAY_INPUTS;
|
|
34
|
+
|
|
35
|
+
protected override renderView(props: object): IDescriptor | null {
|
|
36
|
+
return renderVideoAirPlayButton(props);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { DescriptorOutlet, NativeViewBase } from '@symbiote-native/angular';
|
|
3
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
4
|
+
import { createVideoView } from '../../core';
|
|
5
|
+
import type { IVideoViewProps } from '../../core';
|
|
6
|
+
|
|
7
|
+
// One list is the inputs of the component and the props it hands to the native view
|
|
8
|
+
const VIDEO_VIEW_INPUTS = [
|
|
9
|
+
'player',
|
|
10
|
+
'nativeControls',
|
|
11
|
+
'contentFit',
|
|
12
|
+
'fullscreenOptions',
|
|
13
|
+
'showsTimecodes',
|
|
14
|
+
'requiresLinearPlayback',
|
|
15
|
+
'buttonOptions',
|
|
16
|
+
'surfaceType',
|
|
17
|
+
'contentPosition',
|
|
18
|
+
'onPictureInPictureStart',
|
|
19
|
+
'onPictureInPictureStop',
|
|
20
|
+
'allowsPictureInPicture',
|
|
21
|
+
'startsPictureInPictureAutomatically',
|
|
22
|
+
'allowsVideoFrameAnalysis',
|
|
23
|
+
'onFullscreenEnter',
|
|
24
|
+
'onFullscreenExit',
|
|
25
|
+
'onFirstFrameRender',
|
|
26
|
+
'useExoShutter',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
// Types the inputs for the template, the decorators that would do it are replaced by `inputs`
|
|
30
|
+
export interface VideoView extends Partial<
|
|
31
|
+
Pick<IVideoViewProps, (typeof VIDEO_VIEW_INPUTS)[number]>
|
|
32
|
+
> {}
|
|
33
|
+
|
|
34
|
+
/** Angular twin of `expo-video`'s `VideoView` */
|
|
35
|
+
@Component({
|
|
36
|
+
selector: 'VideoView',
|
|
37
|
+
standalone: true,
|
|
38
|
+
imports: [DescriptorOutlet],
|
|
39
|
+
inputs: [...VIDEO_VIEW_INPUTS],
|
|
40
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
41
|
+
template: `@if (descriptor; as node) {
|
|
42
|
+
<symbiote-descriptor-outlet [node]="node" />
|
|
43
|
+
}`,
|
|
44
|
+
})
|
|
45
|
+
export class VideoView extends NativeViewBase {
|
|
46
|
+
private readonly view = createVideoView(() => this.hostNode());
|
|
47
|
+
|
|
48
|
+
// The functions of the view are members of the component, a `@ViewChild` reaches them
|
|
49
|
+
readonly enterFullscreen = this.view.handle.enterFullscreen;
|
|
50
|
+
readonly exitFullscreen = this.view.handle.exitFullscreen;
|
|
51
|
+
readonly startPictureInPicture = this.view.handle.startPictureInPicture;
|
|
52
|
+
readonly stopPictureInPicture = this.view.handle.stopPictureInPicture;
|
|
53
|
+
|
|
54
|
+
protected override readonly propNames = VIDEO_VIEW_INPUTS;
|
|
55
|
+
|
|
56
|
+
protected override renderView(props: object): IDescriptor | null {
|
|
57
|
+
return this.view.render(props);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Apart from `native-module`, which resolves the module on import: the views need the name too
|
|
2
|
+
|
|
3
|
+
export const VIDEO_MODULE_NAME = 'ExpoVideo';
|
|
4
|
+
|
|
5
|
+
/** Upstream's label for its `UnavailabilityError` */
|
|
6
|
+
export const VIDEO_PACKAGE_NAME = 'expo-video';
|
|
7
|
+
|
|
8
|
+
export const VIDEO_VIEW_NAMES = {
|
|
9
|
+
/** The one iOS view */
|
|
10
|
+
video: 'VideoView',
|
|
11
|
+
/** The Android default, renders into a `SurfaceView` */
|
|
12
|
+
surface: 'SurfaceVideoView',
|
|
13
|
+
/** Android, for overlapping views */
|
|
14
|
+
texture: 'TextureVideoView',
|
|
15
|
+
airPlayButton: 'VideoAirPlayButtonView',
|
|
16
|
+
} as const;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export { VIDEO_MODULE_NAME } from './constants';
|
|
2
|
+
export type {
|
|
3
|
+
IVideoAudioMixingMode,
|
|
4
|
+
IVideoAudioTrack,
|
|
5
|
+
IVideoBufferOptions,
|
|
6
|
+
IVideoContentType,
|
|
7
|
+
IVideoDrmOptions,
|
|
8
|
+
IVideoDrmType,
|
|
9
|
+
IVideoMetadata,
|
|
10
|
+
IVideoPlayerBuilderOptions,
|
|
11
|
+
IVideoPlayerError,
|
|
12
|
+
IVideoPlayerEvents,
|
|
13
|
+
IVideoPlayerStatus,
|
|
14
|
+
IVideoRange,
|
|
15
|
+
IVideoScrubbingModeOptions,
|
|
16
|
+
IVideoSeekTolerance,
|
|
17
|
+
IVideoSize,
|
|
18
|
+
IVideoSource,
|
|
19
|
+
IVideoSourceObject,
|
|
20
|
+
IVideoSubtitleTrack,
|
|
21
|
+
IVideoThumbnailOptions,
|
|
22
|
+
IVideoTrack,
|
|
23
|
+
VideoPlayer,
|
|
24
|
+
} from './player-types';
|
|
25
|
+
export { createVideoPlayer } from './video-player';
|
|
26
|
+
export {
|
|
27
|
+
createVideoPlayerController,
|
|
28
|
+
type IVideoPlayerController,
|
|
29
|
+
} from './video-player-controller';
|
|
30
|
+
export {
|
|
31
|
+
clearVideoCacheAsync,
|
|
32
|
+
getCurrentVideoCacheSize,
|
|
33
|
+
isPictureInPictureSupported,
|
|
34
|
+
setVideoCacheSizeAsync,
|
|
35
|
+
} from './video-module';
|
|
36
|
+
export { parseSource } from './video-source';
|
|
37
|
+
export { VideoThumbnail } from './video-thumbnail';
|
|
38
|
+
export {
|
|
39
|
+
createVideoView,
|
|
40
|
+
type IVideoView,
|
|
41
|
+
type IVideoViewHandle,
|
|
42
|
+
} from './video-view';
|
|
43
|
+
export {
|
|
44
|
+
renderVideoAirPlayButton,
|
|
45
|
+
videoAirPlayButtonViewName,
|
|
46
|
+
} from './video-airplay-button';
|
|
47
|
+
export type {
|
|
48
|
+
IVideoAirPlayButtonProps,
|
|
49
|
+
IVideoButtonOptions,
|
|
50
|
+
IVideoContentFit,
|
|
51
|
+
IVideoFullscreenOptions,
|
|
52
|
+
IVideoFullscreenOrientation,
|
|
53
|
+
IVideoKeepFullscreenOnPiPStop,
|
|
54
|
+
IVideoSurfaceType,
|
|
55
|
+
IVideoViewProps,
|
|
56
|
+
} from './view-types';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
import { VIDEO_MODULE_NAME } from './constants';
|
|
3
|
+
import type { VideoPlayer, VideoThumbnail } from './player-types';
|
|
4
|
+
|
|
5
|
+
export type INativeVideoModule = {
|
|
6
|
+
VideoPlayer: typeof VideoPlayer;
|
|
7
|
+
VideoThumbnail: typeof VideoThumbnail;
|
|
8
|
+
isPictureInPictureSupported(): boolean;
|
|
9
|
+
setVideoCacheSizeAsync(sizeBytes: number): Promise<void>;
|
|
10
|
+
clearVideoCacheAsync(): Promise<void>;
|
|
11
|
+
getCurrentVideoCacheSize(): number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const expoVideo =
|
|
15
|
+
requireNativeModule<INativeVideoModule>(VIDEO_MODULE_NAME);
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { SharedObject, SharedRef } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
export type IVideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';
|
|
4
|
+
|
|
5
|
+
export type IVideoContentType =
|
|
6
|
+
'auto' | 'progressive' | 'hls' | 'dash' | 'smoothStreaming';
|
|
7
|
+
|
|
8
|
+
export type IVideoDrmType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';
|
|
9
|
+
|
|
10
|
+
export type IVideoDrmOptions = {
|
|
11
|
+
type: IVideoDrmType;
|
|
12
|
+
licenseServer: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
/** Android only */
|
|
15
|
+
multiKey?: boolean;
|
|
16
|
+
/** iOS only */
|
|
17
|
+
contentId?: string;
|
|
18
|
+
/** iOS only */
|
|
19
|
+
certificateUrl?: string;
|
|
20
|
+
/** iOS only */
|
|
21
|
+
base64CertificateData?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type IVideoMetadata = {
|
|
25
|
+
title?: string;
|
|
26
|
+
artist?: string;
|
|
27
|
+
artwork?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type IVideoSourceObject = {
|
|
31
|
+
uri?: string;
|
|
32
|
+
/** A `require`d asset, `uri` wins when both are set */
|
|
33
|
+
assetId?: number;
|
|
34
|
+
drm?: IVideoDrmOptions;
|
|
35
|
+
metadata?: IVideoMetadata;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
/** Caches the video on disk, off by default */
|
|
38
|
+
useCaching?: boolean;
|
|
39
|
+
contentType?: IVideoContentType;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A URI, a `require`d asset or an object, `null` unloads the player */
|
|
43
|
+
export type IVideoSource = string | number | null | IVideoSourceObject;
|
|
44
|
+
|
|
45
|
+
export type IVideoPlayerError = { message: string };
|
|
46
|
+
|
|
47
|
+
export type IVideoAudioMixingMode =
|
|
48
|
+
'mixWithOthers' | 'duckOthers' | 'auto' | 'doNotMix';
|
|
49
|
+
|
|
50
|
+
export type IVideoBufferOptions = {
|
|
51
|
+
/** iOS only, in seconds */
|
|
52
|
+
readonly preferredForwardBufferDuration?: number;
|
|
53
|
+
/** iOS only */
|
|
54
|
+
readonly waitsToMinimizeStalling?: boolean;
|
|
55
|
+
/** Android only, in seconds */
|
|
56
|
+
readonly minBufferForPlayback?: number;
|
|
57
|
+
/** Android only, in bytes */
|
|
58
|
+
readonly maxBufferBytes?: number | null;
|
|
59
|
+
/** Android only */
|
|
60
|
+
readonly prioritizeTimeOverSizeThreshold?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type IVideoTextTrack = {
|
|
64
|
+
id?: string;
|
|
65
|
+
language: string;
|
|
66
|
+
label: string;
|
|
67
|
+
name?: string;
|
|
68
|
+
isDefault?: boolean;
|
|
69
|
+
autoSelect?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type IVideoSubtitleTrack = IVideoTextTrack;
|
|
73
|
+
|
|
74
|
+
export type IVideoAudioTrack = IVideoTextTrack;
|
|
75
|
+
|
|
76
|
+
export type IVideoSize = { width: number; height: number };
|
|
77
|
+
|
|
78
|
+
export type IVideoRange = 'sdr' | 'hlg' | 'pq';
|
|
79
|
+
|
|
80
|
+
export type IVideoTrack = {
|
|
81
|
+
id: string;
|
|
82
|
+
url: string | null;
|
|
83
|
+
size: IVideoSize;
|
|
84
|
+
mimeType: string | null;
|
|
85
|
+
isSupported: boolean;
|
|
86
|
+
bitrate: number | null;
|
|
87
|
+
averageBitrate: number | null;
|
|
88
|
+
peakBitrate: number | null;
|
|
89
|
+
frameRate: number | null;
|
|
90
|
+
videoRange: IVideoRange;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type IVideoSeekTolerance = {
|
|
94
|
+
/** iOS only, in seconds */
|
|
95
|
+
toleranceBefore?: number;
|
|
96
|
+
/** iOS only, in seconds */
|
|
97
|
+
toleranceAfter?: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type IVideoScrubbingModeOptions = {
|
|
101
|
+
/** Android only */
|
|
102
|
+
scrubbingModeEnabled?: boolean;
|
|
103
|
+
increaseCodecOperatingRate?: boolean;
|
|
104
|
+
enableDynamicScheduling?: boolean;
|
|
105
|
+
useDecodeOnlyFlag?: boolean;
|
|
106
|
+
allowSkippingMediaCodecFlush?: boolean;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Options of the Android player builder, applied before the native constructor runs */
|
|
110
|
+
export type IVideoPlayerBuilderOptions = {
|
|
111
|
+
/** In seconds */
|
|
112
|
+
seekBackwardIncrement?: number;
|
|
113
|
+
/** In seconds */
|
|
114
|
+
seekForwardIncrement?: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type IVideoThumbnailOptions = {
|
|
118
|
+
maxWidth?: number;
|
|
119
|
+
maxHeight?: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type IVideoPlayerEvents = {
|
|
123
|
+
statusChange(payload: {
|
|
124
|
+
status: IVideoPlayerStatus;
|
|
125
|
+
oldStatus?: IVideoPlayerStatus;
|
|
126
|
+
error?: IVideoPlayerError;
|
|
127
|
+
}): void;
|
|
128
|
+
playingChange(payload: { isPlaying: boolean; oldIsPlaying?: boolean }): void;
|
|
129
|
+
playbackRateChange(payload: {
|
|
130
|
+
playbackRate: number;
|
|
131
|
+
oldPlaybackRate?: number;
|
|
132
|
+
}): void;
|
|
133
|
+
volumeChange(payload: { volume: number; oldVolume?: number }): void;
|
|
134
|
+
mutedChange(payload: { muted: boolean; oldMuted?: boolean }): void;
|
|
135
|
+
playToEnd(): void;
|
|
136
|
+
timeUpdate(payload: {
|
|
137
|
+
currentTime: number;
|
|
138
|
+
currentLiveTimestamp: number | null;
|
|
139
|
+
currentOffsetFromLive: number | null;
|
|
140
|
+
bufferedPosition: number;
|
|
141
|
+
}): void;
|
|
142
|
+
sourceChange(payload: {
|
|
143
|
+
source: IVideoSource;
|
|
144
|
+
oldSource?: IVideoSource;
|
|
145
|
+
}): void;
|
|
146
|
+
availableSubtitleTracksChange(payload: {
|
|
147
|
+
availableSubtitleTracks: IVideoSubtitleTrack[];
|
|
148
|
+
oldAvailableSubtitleTracks?: IVideoSubtitleTrack[];
|
|
149
|
+
}): void;
|
|
150
|
+
subtitleTrackChange(payload: {
|
|
151
|
+
subtitleTrack: IVideoSubtitleTrack | null;
|
|
152
|
+
oldSubtitleTrack?: IVideoSubtitleTrack | null;
|
|
153
|
+
}): void;
|
|
154
|
+
availableAudioTracksChange(payload: {
|
|
155
|
+
availableAudioTracks: IVideoAudioTrack[];
|
|
156
|
+
oldAvailableAudioTracks?: IVideoAudioTrack[];
|
|
157
|
+
}): void;
|
|
158
|
+
audioTrackChange(payload: {
|
|
159
|
+
audioTrack: IVideoAudioTrack | null;
|
|
160
|
+
oldAudioTrack?: IVideoAudioTrack | null;
|
|
161
|
+
}): void;
|
|
162
|
+
videoTrackChange(payload: {
|
|
163
|
+
videoTrack: IVideoTrack | null;
|
|
164
|
+
oldVideoTrack?: IVideoTrack | null;
|
|
165
|
+
}): void;
|
|
166
|
+
sourceLoad(payload: {
|
|
167
|
+
videoSource: IVideoSource | null;
|
|
168
|
+
duration: number;
|
|
169
|
+
availableVideoTracks: IVideoTrack[];
|
|
170
|
+
availableSubtitleTracks: IVideoSubtitleTrack[];
|
|
171
|
+
availableAudioTracks: IVideoAudioTrack[];
|
|
172
|
+
}): void;
|
|
173
|
+
isExternalPlaybackActiveChange(payload: {
|
|
174
|
+
isExternalPlaybackActive: boolean;
|
|
175
|
+
oldIsExternalPlaybackActive?: boolean;
|
|
176
|
+
}): void;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** The native thumbnail, `expo-image` takes it as an image source */
|
|
180
|
+
export declare class VideoThumbnail extends SharedRef<'image'> {
|
|
181
|
+
width: number;
|
|
182
|
+
height: number;
|
|
183
|
+
/** In seconds */
|
|
184
|
+
requestedTime: number;
|
|
185
|
+
/** In seconds, iOS only */
|
|
186
|
+
actualTime: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export declare class VideoPlayer extends SharedObject<IVideoPlayerEvents> {
|
|
190
|
+
readonly playing: boolean;
|
|
191
|
+
loop: boolean;
|
|
192
|
+
/** iOS only */
|
|
193
|
+
allowsExternalPlayback: boolean;
|
|
194
|
+
audioMixingMode: IVideoAudioMixingMode;
|
|
195
|
+
muted: boolean;
|
|
196
|
+
/** In seconds, assigning seeks */
|
|
197
|
+
currentTime: number;
|
|
198
|
+
readonly currentLiveTimestamp: number | null;
|
|
199
|
+
readonly currentOffsetFromLive: number | null;
|
|
200
|
+
targetOffsetFromLive: number;
|
|
201
|
+
readonly duration: number;
|
|
202
|
+
volume: number;
|
|
203
|
+
preservesPitch: boolean;
|
|
204
|
+
/** In seconds, `0` turns the `timeUpdate` event off */
|
|
205
|
+
timeUpdateEventInterval: number;
|
|
206
|
+
playbackRate: number;
|
|
207
|
+
keepScreenOnWhilePlaying: boolean;
|
|
208
|
+
readonly isLive: boolean;
|
|
209
|
+
readonly status: IVideoPlayerStatus;
|
|
210
|
+
showNowPlayingNotification: boolean;
|
|
211
|
+
staysActiveInBackground: boolean;
|
|
212
|
+
readonly bufferedPosition: number;
|
|
213
|
+
bufferOptions: IVideoBufferOptions;
|
|
214
|
+
subtitleTrack: IVideoSubtitleTrack | null;
|
|
215
|
+
audioTrack: IVideoAudioTrack | null;
|
|
216
|
+
readonly availableAudioTracks: IVideoAudioTrack[];
|
|
217
|
+
readonly availableSubtitleTracks: IVideoSubtitleTrack[];
|
|
218
|
+
readonly videoTrack: IVideoTrack | null;
|
|
219
|
+
readonly availableVideoTracks: IVideoTrack[];
|
|
220
|
+
readonly isExternalPlaybackActive: boolean;
|
|
221
|
+
seekTolerance: IVideoSeekTolerance;
|
|
222
|
+
scrubbingModeOptions: IVideoScrubbingModeOptions;
|
|
223
|
+
|
|
224
|
+
constructor(
|
|
225
|
+
source: IVideoSource,
|
|
226
|
+
useSynchronousReplace?: boolean,
|
|
227
|
+
playerBuilderOptions?: IVideoPlayerBuilderOptions,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
play(): void;
|
|
231
|
+
pause(): void;
|
|
232
|
+
/** On iOS it loads the asset on the main thread, prefer `replaceAsync` */
|
|
233
|
+
replace(source: IVideoSource, disableWarning?: boolean): void;
|
|
234
|
+
replaceAsync(source: IVideoSource): Promise<void>;
|
|
235
|
+
seekBy(seconds: number): void;
|
|
236
|
+
replay(): void;
|
|
237
|
+
generateThumbnailsAsync(
|
|
238
|
+
times: number | number[],
|
|
239
|
+
options?: IVideoThumbnailOptions,
|
|
240
|
+
): Promise<VideoThumbnail[]>;
|
|
241
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Platform, requireNativeViewManager } from 'expo-modules-core';
|
|
2
|
+
import { el } from '@symbiote-native/components';
|
|
3
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
4
|
+
import { defineExpoNativeView } from '@symbiote-native/engine';
|
|
5
|
+
import { VIDEO_MODULE_NAME, VIDEO_VIEW_NAMES } from './constants';
|
|
6
|
+
import type { IVideoAirPlayButtonProps } from './view-types';
|
|
7
|
+
|
|
8
|
+
// Регистрация идёт при рендере: побочный эффект барреля теряется в release
|
|
9
|
+
const airPlayButton = defineExpoNativeView(
|
|
10
|
+
requireNativeViewManager,
|
|
11
|
+
VIDEO_MODULE_NAME,
|
|
12
|
+
VIDEO_VIEW_NAMES.airPlayButton,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const videoAirPlayButtonViewName = airPlayButton.name;
|
|
16
|
+
|
|
17
|
+
const MIN_SIZE = { minWidth: 30, minHeight: 30 };
|
|
18
|
+
|
|
19
|
+
// What only the native route picker takes, the plain fallback view must not get it
|
|
20
|
+
const AIRPLAY_ONLY_PROPS = new Set<string>([
|
|
21
|
+
'tint',
|
|
22
|
+
'activeTint',
|
|
23
|
+
'prioritizeVideoDevices',
|
|
24
|
+
'onBeginPresentingRoutes',
|
|
25
|
+
'onEndPresentingRoutes',
|
|
26
|
+
] as const satisfies readonly (keyof IVideoAirPlayButtonProps)[]);
|
|
27
|
+
|
|
28
|
+
function isAirPlayAvailable(): boolean {
|
|
29
|
+
return (
|
|
30
|
+
Platform.select({ ios: true, default: false }) &&
|
|
31
|
+
airPlayButton.ensureRegistered()
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A plain view with the minimum size where the system route picker does not exist */
|
|
36
|
+
export function renderVideoAirPlayButton(props: object): IDescriptor {
|
|
37
|
+
const style = [MIN_SIZE, Reflect.get(props, 'style')];
|
|
38
|
+
if (isAirPlayAvailable()) {
|
|
39
|
+
return el(airPlayButton.name(), { ...props, style });
|
|
40
|
+
}
|
|
41
|
+
const viewProps = Object.entries(props).filter(
|
|
42
|
+
([key]) => !AIRPLAY_ONLY_PROPS.has(key),
|
|
43
|
+
);
|
|
44
|
+
return el('view', { ...Object.fromEntries(viewProps), style });
|
|
45
|
+
}
|