chore: rework examples (#4225)
* remove unused examples * init bare example with test app * add react-native-video * add test app suport in expo plugin * expo plugin: skip keys that are already in pod file * fix podfile * add src files * fix metro config * finalize react native test app configuration * init expo example * remove old examples * add guide for example * Add link to examples apps in docs * adopt bare example to CI tests * update CI workflows * CI build lib after node_modules install * fix examples readme * fix iOS CI * Add Example for DRM * Update examples/README.md * fix links * update examples README * sync example code * update README
This commit is contained in:
16
examples/bare/.gitignore
vendored
Normal file
16
examples/bare/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
*.binlog
|
||||
*.hprof
|
||||
*.xcworkspace/
|
||||
*.zip
|
||||
.DS_Store
|
||||
.gradle/
|
||||
.idea/
|
||||
.vs/
|
||||
.xcode.env
|
||||
Pods/
|
||||
build/
|
||||
dist/*
|
||||
!dist/.gitignore
|
||||
local.properties
|
||||
msbuild.binlog
|
||||
node_modules/
|
1
examples/bare/.watchmanconfig
Normal file
1
examples/bare/.watchmanconfig
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
48
examples/bare/android/build.gradle
Normal file
48
examples/bare/android/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
buildscript {
|
||||
apply(from: {
|
||||
def searchDir = rootDir.toPath()
|
||||
do {
|
||||
def p = searchDir.resolve("node_modules/react-native-test-app/android/dependencies.gradle")
|
||||
if (p.toFile().exists()) {
|
||||
return p.toRealPath().toString()
|
||||
}
|
||||
} while (searchDir = searchDir.getParent())
|
||||
throw new GradleException("Could not find `react-native-test-app`");
|
||||
}())
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
getReactNativeDependencies().each { dependency ->
|
||||
classpath(dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
// For CI builds
|
||||
useExoplayerIMA = System.getenv("RNV_SAMPLE_ENABLE_ADS") ?: false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url({
|
||||
def searchDir = rootDir.toPath()
|
||||
do {
|
||||
def p = searchDir.resolve("node_modules/react-native/android")
|
||||
if (p.toFile().exists()) {
|
||||
return p.toRealPath().toString()
|
||||
}
|
||||
} while (searchDir = searchDir.getParent())
|
||||
throw new GradleException("Could not find `react-native`");
|
||||
}())
|
||||
}
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
53
examples/bare/android/gradle.properties
Normal file
53
examples/bare/android/gradle.properties
Normal file
@@ -0,0 +1,53 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the Gradle Daemon. The setting is
|
||||
# particularly useful for configuring JVM memory settings for build performance.
|
||||
# This does not affect the JVM settings for the Gradle client VM.
|
||||
# The default is `-Xmx512m -XX:MaxMetaspaceSize=256m`.
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute
|
||||
# projects in parallel. To learn more about parallel task execution, see the
|
||||
# section on Gradle build performance:
|
||||
# https://docs.gradle.org/current/userguide/performance.html#parallel_execution.
|
||||
# Default is `false`.
|
||||
#org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Jetifier randomly fails on these libraries
|
||||
android.jetifier.ignorelist=hermes-android,react-android
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
# Note that this is incompatible with web debugging.
|
||||
#newArchEnabled=true
|
||||
#bridgelessEnabled=true
|
||||
|
||||
# Uncomment the line below to build React Native from source.
|
||||
#react.buildFromSource=true
|
||||
|
||||
# Version of Android NDK to build against.
|
||||
#ANDROID_NDK_VERSION=26.1.10909125
|
||||
|
||||
# Version of Kotlin to build against.
|
||||
#KOTLIN_VERSION=1.8.22
|
BIN
examples/bare/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
examples/bare/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
examples/bare/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
examples/bare/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
249
examples/bare/android/gradlew
vendored
Executable file
249
examples/bare/android/gradlew
vendored
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
92
examples/bare/android/gradlew.bat
vendored
Normal file
92
examples/bare/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
21
examples/bare/android/settings.gradle
Normal file
21
examples/bare/android/settings.gradle
Normal file
@@ -0,0 +1,21 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "BareExample"
|
||||
|
||||
apply(from: {
|
||||
def searchDir = rootDir.toPath()
|
||||
do {
|
||||
def p = searchDir.resolve("node_modules/react-native-test-app/test-app.gradle")
|
||||
if (p.toFile().exists()) {
|
||||
return p.toRealPath().toString()
|
||||
}
|
||||
} while (searchDir = searchDir.getParent())
|
||||
throw new GradleException("Could not find `react-native-test-app`");
|
||||
}())
|
||||
applyTestAppSettings(settings)
|
54
examples/bare/app.json
Normal file
54
examples/bare/app.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "BareExample",
|
||||
"displayName": "BareExample",
|
||||
"components": [
|
||||
{
|
||||
"appKey": "BareExample",
|
||||
"displayName": "Basic Video Example"
|
||||
},
|
||||
{
|
||||
"appKey": "DRMExample",
|
||||
"displayName": "DRM Example"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"android": [
|
||||
"dist/res",
|
||||
"dist/main.android.jsbundle"
|
||||
],
|
||||
"ios": [
|
||||
"dist/assets",
|
||||
"dist/main.ios.jsbundle"
|
||||
],
|
||||
"macos": [
|
||||
"dist/assets",
|
||||
"dist/main.macos.jsbundle"
|
||||
],
|
||||
"visionos": [
|
||||
"dist/assets",
|
||||
"dist/main.visionos.jsbundle"
|
||||
],
|
||||
"windows": [
|
||||
"dist/assets",
|
||||
"dist/main.windows.bundle"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
"reactNativeTestApp": true,
|
||||
"enableNotificationControls": true,
|
||||
"enableBackgroundAudio": true,
|
||||
"enableADSExtension": false,
|
||||
"enableCacheExtension": false,
|
||||
"androidExtensions": {
|
||||
"useExoplayerRtsp": true,
|
||||
"useExoplayerSmoothStreaming": true,
|
||||
"useExoplayerHls": true,
|
||||
"useExoplayerDash": true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
3
examples/bare/babel.config.js
Normal file
3
examples/bare/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
};
|
11
examples/bare/index.js
Normal file
11
examples/bare/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {AppRegistry} from 'react-native';
|
||||
import BasicExample from './src/BasicExample';
|
||||
import {name as appName} from './app.json';
|
||||
import DRMExample from './src/DRMExample';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => BasicExample);
|
||||
AppRegistry.registerComponent('DRMExample', () => DRMExample);
|
23
examples/bare/ios/Podfile
Normal file
23
examples/bare/ios/Podfile
Normal file
@@ -0,0 +1,23 @@
|
||||
ws_dir = Pathname.new(__dir__)
|
||||
ws_dir = ws_dir.parent until
|
||||
File.exist?("#{ws_dir}/node_modules/react-native-test-app/test_app.rb") ||
|
||||
ws_dir.expand_path.to_s == '/'
|
||||
require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb"
|
||||
|
||||
workspace 'BareExample.xcworkspace'
|
||||
|
||||
use_test_app!
|
||||
|
||||
# This is used by CI to test different configurations
|
||||
# If you want to enable it look to README.md
|
||||
if ENV['RNV_SAMPLE_ENABLE_ADS']
|
||||
$RNVideoUseGoogleIMA = true
|
||||
end
|
||||
if ENV['RNV_SAMPLE_VIDEO_CACHING']
|
||||
$RNVideoUseVideoCaching = true
|
||||
end
|
||||
|
||||
# Chache dependencies need to have modular headers
|
||||
if defined?($RNVideoUseVideoCaching)
|
||||
use_modular_headers!
|
||||
end
|
1267
examples/bare/ios/Podfile.lock
Normal file
1267
examples/bare/ios/Podfile.lock
Normal file
File diff suppressed because it is too large
Load Diff
20
examples/bare/metro.config.js
Normal file
20
examples/bare/metro.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const path = require('path');
|
||||
const {makeMetroConfig} = require('@rnx-kit/metro-config');
|
||||
|
||||
module.exports = makeMetroConfig({
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolver: {
|
||||
enableSymlinks: true,
|
||||
},
|
||||
watchFolders: [
|
||||
path.join(__dirname, 'node_modules', 'react-native-video'),
|
||||
path.resolve(__dirname, '../..'),
|
||||
],
|
||||
});
|
52
examples/bare/package.json
Normal file
52
examples/bare/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "BareExample",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"android": "react-native run-android",
|
||||
"build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist/res",
|
||||
"build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist",
|
||||
"build:visionos": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.visionos.jsbundle --assets-dest dist",
|
||||
"build:windows": "npm run mkdist && react-native bundle --entry-file index.js --platform windows --dev true --bundle-output dist/main.windows.bundle --assets-dest dist",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"visionos": "react-native run-visionos",
|
||||
"windows": "react-native run-windows --sln windows/BareExample.sln"
|
||||
},
|
||||
"dependencies": {
|
||||
"@callstack/react-native-visionos": "^0.73.0",
|
||||
"@react-native-picker/picker": "2.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-video": "link:../..",
|
||||
"react-native-windows": "^0.73.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/config-plugins": "^8.0.10",
|
||||
"@react-native/babel-preset": "0.73.19",
|
||||
"@react-native/eslint-config": "0.73.2",
|
||||
"@react-native/metro-config": "0.73.3",
|
||||
"@react-native/typescript-config": "0.73.1",
|
||||
"@rnx-kit/metro-config": "^2.0.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"babel-jest": "^29.6.3",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "2.8.8",
|
||||
"react-native-test-app": "^3.10.14",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
diff --git a/node_modules/@react-native-picker/picker/RNCPicker.podspec b/node_modules/@react-native-picker/picker/RNCPicker.podspec
|
||||
index bfdf16c..bdc9c7c 100644
|
||||
--- a/node_modules/@react-native-picker/picker/RNCPicker.podspec
|
||||
+++ b/node_modules/@react-native-picker/picker/RNCPicker.podspec
|
||||
@@ -12,7 +12,7 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.authors = package['author']
|
||||
s.homepage = package['homepage']
|
||||
- s.platforms = { :ios => "9.0", :osx => "10.14" }
|
||||
+ s.platforms = { :ios => "9.0", :osx => "10.14", :visionos => "1.0" }
|
||||
|
||||
s.source = { :git => "https://github.com/react-native-picker/picker.git", :tag => "v#{s.version}" }
|
||||
|
||||
@@ -25,6 +25,7 @@ Pod::Spec.new do |s|
|
||||
else
|
||||
s.ios.source_files = "ios/**/*.{h,m,mm}"
|
||||
s.osx.source_files = "macos/**/*.{h,m,mm}"
|
||||
+ s.visionos.source_files = "ios/**/*.{h,m,mm}"
|
||||
end
|
||||
|
||||
s.dependency 'React-Core'
|
23
examples/bare/react-native.config.js
Normal file
23
examples/bare/react-native.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const project = (() => {
|
||||
try {
|
||||
const { configureProjects } = require("react-native-test-app");
|
||||
return configureProjects({
|
||||
android: {
|
||||
sourceDir: "android",
|
||||
},
|
||||
ios: {
|
||||
sourceDir: "ios",
|
||||
},
|
||||
windows: {
|
||||
sourceDir: "windows",
|
||||
solutionFile: "windows/BareExample.sln",
|
||||
},
|
||||
});
|
||||
} catch (_) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
...(project ? { project } : undefined),
|
||||
};
|
345
examples/bare/src/BasicExample.tsx
Normal file
345
examples/bare/src/BasicExample.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import React, {type FC, useCallback, useRef, useState, useEffect} from 'react';
|
||||
|
||||
import {Platform, TouchableOpacity, View, StatusBar} from 'react-native';
|
||||
|
||||
import Video, {
|
||||
VideoRef,
|
||||
SelectedVideoTrackType,
|
||||
BufferingStrategyType,
|
||||
SelectedTrackType,
|
||||
ResizeMode,
|
||||
type AudioTrack,
|
||||
type OnAudioTracksData,
|
||||
type OnLoadData,
|
||||
type OnProgressData,
|
||||
type OnTextTracksData,
|
||||
type OnVideoAspectRatioData,
|
||||
type TextTrack,
|
||||
type OnBufferData,
|
||||
type OnAudioFocusChangedData,
|
||||
type OnVideoErrorData,
|
||||
type OnTextTrackDataChangedData,
|
||||
type OnSeekData,
|
||||
type OnPlaybackStateChangedData,
|
||||
type OnPlaybackRateChangeData,
|
||||
type OnVideoTracksData,
|
||||
type ReactVideoSource,
|
||||
type VideoTrack,
|
||||
type SelectedTrack,
|
||||
type SelectedVideoTrack,
|
||||
type EnumValues,
|
||||
OnBandwidthUpdateData,
|
||||
ControlsStyles,
|
||||
} from 'react-native-video';
|
||||
import styles from './styles';
|
||||
import {type AdditionalSourceInfo} from './types';
|
||||
import {
|
||||
bufferConfig,
|
||||
isAndroid,
|
||||
srcList,
|
||||
textTracksSelectionBy,
|
||||
audioTracksSelectionBy,
|
||||
} from './constants';
|
||||
import {Overlay, toast, VideoLoader} from './components';
|
||||
|
||||
const BasicExample = () => {
|
||||
const [rate, setRate] = useState(1);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [resizeMode, setResizeMode] = useState<EnumValues<ResizeMode>>(
|
||||
ResizeMode.CONTAIN,
|
||||
);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [_, setVideoSize] = useState({videoWidth: 0, videoHeight: 0});
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [fullscreen, setFullscreen] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]);
|
||||
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
||||
const [videoTracks, setVideoTracks] = useState<VideoTrack[]>([]);
|
||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>(undefined);
|
||||
const [selectedTextTrack, setSelectedTextTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>(undefined);
|
||||
const [selectedVideoTrack, setSelectedVideoTrack] =
|
||||
useState<SelectedVideoTrack>({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
const [srcListId, setSrcListId] = useState(0);
|
||||
const [repeat, setRepeat] = useState(false);
|
||||
const [controls, setControls] = useState(false);
|
||||
const [useCache, setUseCache] = useState(false);
|
||||
const [showPoster, setShowPoster] = useState<boolean>(false);
|
||||
const [showNotificationControls, setShowNotificationControls] =
|
||||
useState(false);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
|
||||
const currentSrc = srcList[srcListId];
|
||||
const additional = currentSrc as AdditionalSourceInfo;
|
||||
|
||||
const goToChannel = useCallback((channel: number) => {
|
||||
setSrcListId(channel);
|
||||
setDuration(0);
|
||||
setCurrentTime(0);
|
||||
setVideoSize({videoWidth: 0, videoHeight: 0});
|
||||
setIsLoading(false);
|
||||
setAudioTracks([]);
|
||||
setTextTracks([]);
|
||||
setSelectedAudioTrack(undefined);
|
||||
setSelectedTextTrack(undefined);
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const channelUp = useCallback(() => {
|
||||
console.log('channel up');
|
||||
goToChannel((srcListId + 1) % srcList.length);
|
||||
}, [goToChannel, srcListId]);
|
||||
|
||||
const channelDown = useCallback(() => {
|
||||
console.log('channel down');
|
||||
goToChannel((srcListId + srcList.length - 1) % srcList.length);
|
||||
}, [goToChannel, srcListId]);
|
||||
|
||||
const onAudioTracks = (data: OnAudioTracksData) => {
|
||||
console.log('onAudioTracks', data);
|
||||
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
|
||||
return x.selected;
|
||||
});
|
||||
let value;
|
||||
if (audioTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = selectedTrack?.index;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = selectedTrack?.language;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = selectedTrack?.title;
|
||||
}
|
||||
setAudioTracks(data.audioTracks);
|
||||
setSelectedAudioTrack({
|
||||
type: audioTracksSelectionBy,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onVideoTracks = (data: OnVideoTracksData) => {
|
||||
console.log('onVideoTracks', data.videoTracks);
|
||||
setVideoTracks(data.videoTracks);
|
||||
};
|
||||
|
||||
const onTextTracks = (data: OnTextTracksData) => {
|
||||
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
|
||||
return x?.selected;
|
||||
});
|
||||
|
||||
setTextTracks(data.textTracks);
|
||||
let value;
|
||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = selectedTrack?.index;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = selectedTrack?.language;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = selectedTrack?.title;
|
||||
}
|
||||
setSelectedTextTrack({
|
||||
type: textTracksSelectionBy,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onLoad = (data: OnLoadData) => {
|
||||
setDuration(data.duration);
|
||||
onAudioTracks(data);
|
||||
onTextTracks(data);
|
||||
onVideoTracks(data);
|
||||
};
|
||||
|
||||
const onProgress = (data: OnProgressData) => {
|
||||
setCurrentTime(data.currentTime);
|
||||
};
|
||||
|
||||
const onSeek = (data: OnSeekData) => {
|
||||
setCurrentTime(data.currentTime);
|
||||
setIsSeeking(false);
|
||||
};
|
||||
|
||||
const onVideoLoadStart = () => {
|
||||
console.log('onVideoLoadStart');
|
||||
setIsLoading(true);
|
||||
};
|
||||
|
||||
const onTextTrackDataChanged = (data: OnTextTrackDataChangedData) => {
|
||||
console.log(`Subtitles: ${JSON.stringify(data, null, 2)}`);
|
||||
};
|
||||
|
||||
const onAspectRatio = (data: OnVideoAspectRatioData) => {
|
||||
console.log('onAspectRadio called ' + JSON.stringify(data));
|
||||
setVideoSize({videoWidth: data.width, videoHeight: data.height});
|
||||
};
|
||||
|
||||
const onVideoBuffer = (param: OnBufferData) => {
|
||||
console.log('onVideoBuffer');
|
||||
setIsLoading(param.isBuffering);
|
||||
};
|
||||
|
||||
const onReadyForDisplay = () => {
|
||||
console.log('onReadyForDisplay');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onAudioBecomingNoisy = () => {
|
||||
setPaused(true);
|
||||
};
|
||||
|
||||
const onAudioFocusChanged = (event: OnAudioFocusChangedData) => {
|
||||
setPaused(!event.hasAudioFocus);
|
||||
};
|
||||
|
||||
const onError = (err: OnVideoErrorData) => {
|
||||
console.log(JSON.stringify(err));
|
||||
toast(true, 'error: ' + JSON.stringify(err));
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
if (!repeat) {
|
||||
channelUp();
|
||||
}
|
||||
};
|
||||
|
||||
const onPlaybackRateChange = (data: OnPlaybackRateChangeData) => {
|
||||
console.log('onPlaybackRateChange', data);
|
||||
};
|
||||
|
||||
const onPlaybackStateChanged = (data: OnPlaybackStateChangedData) => {
|
||||
console.log('onPlaybackStateChanged', data);
|
||||
};
|
||||
|
||||
const onVideoBandwidthUpdate = (data: OnBandwidthUpdateData) => {
|
||||
console.log('onVideoBandwidthUpdate', data);
|
||||
};
|
||||
|
||||
const onFullScreenExit = () => {
|
||||
// iOS pauses video on exit from full screen
|
||||
Platform.OS === 'ios' && setPaused(true);
|
||||
};
|
||||
|
||||
const _renderLoader = showPoster ? () => <VideoLoader /> : undefined;
|
||||
|
||||
const _subtitleStyle = {subtitlesFollowVideo: true};
|
||||
const _controlsStyles: ControlsStyles = {
|
||||
hideNavigationBarOnFullScreenMode: true,
|
||||
hideNotificationBarOnFullScreenMode: true,
|
||||
liveLabel: 'LIVE',
|
||||
};
|
||||
const _bufferConfig = {
|
||||
...bufferConfig,
|
||||
cacheSizeMB: useCache ? 200 : 0,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
videoRef.current?.setSource(currentSrc);
|
||||
}, [currentSrc]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar animated={true} backgroundColor="black" hidden={false} />
|
||||
|
||||
{(srcList[srcListId] as AdditionalSourceInfo)?.noView ? null : (
|
||||
<TouchableOpacity style={viewStyle}>
|
||||
<Video
|
||||
showNotificationControls={showNotificationControls}
|
||||
ref={videoRef}
|
||||
// source={currentSrc as ReactVideoSource}
|
||||
drm={additional?.drm}
|
||||
style={viewStyle}
|
||||
rate={rate}
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
controls={controls}
|
||||
resizeMode={resizeMode}
|
||||
onFullscreenPlayerWillDismiss={onFullScreenExit}
|
||||
onLoad={onLoad}
|
||||
onAudioTracks={onAudioTracks}
|
||||
onTextTracks={onTextTracks}
|
||||
onVideoTracks={onVideoTracks}
|
||||
onTextTrackDataChanged={onTextTrackDataChanged}
|
||||
onProgress={onProgress}
|
||||
onEnd={onEnd}
|
||||
progressUpdateInterval={1000}
|
||||
onError={onError}
|
||||
onAudioBecomingNoisy={onAudioBecomingNoisy}
|
||||
onAudioFocusChanged={onAudioFocusChanged}
|
||||
onLoadStart={onVideoLoadStart}
|
||||
onAspectRatio={onAspectRatio}
|
||||
onReadyForDisplay={onReadyForDisplay}
|
||||
onBuffer={onVideoBuffer}
|
||||
onBandwidthUpdate={onVideoBandwidthUpdate}
|
||||
onSeek={onSeek}
|
||||
repeat={repeat}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
playInBackground={false}
|
||||
bufferConfig={_bufferConfig}
|
||||
preventsDisplaySleepDuringVideoPlayback={true}
|
||||
renderLoader={_renderLoader}
|
||||
onPlaybackRateChange={onPlaybackRateChange}
|
||||
onPlaybackStateChanged={onPlaybackStateChanged}
|
||||
bufferingStrategy={BufferingStrategyType.DEFAULT}
|
||||
debug={{enable: true, thread: true}}
|
||||
subtitleStyle={_subtitleStyle}
|
||||
controlsStyles={_controlsStyles}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Overlay
|
||||
channelDown={channelDown}
|
||||
channelUp={channelUp}
|
||||
ref={videoRef}
|
||||
videoTracks={videoTracks}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
setSelectedTextTrack={setSelectedTextTrack}
|
||||
audioTracks={audioTracks}
|
||||
controls={controls}
|
||||
resizeMode={resizeMode}
|
||||
textTracks={textTracks}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
setSelectedAudioTrack={setSelectedAudioTrack}
|
||||
setSelectedVideoTrack={setSelectedVideoTrack}
|
||||
currentTime={currentTime}
|
||||
setMuted={setMuted}
|
||||
muted={muted}
|
||||
duration={duration}
|
||||
paused={paused}
|
||||
volume={volume}
|
||||
setControls={setControls}
|
||||
showPoster={showPoster}
|
||||
rate={rate}
|
||||
setFullscreen={setFullscreen}
|
||||
setPaused={setPaused}
|
||||
isLoading={isLoading}
|
||||
isSeeking={isSeeking}
|
||||
setIsSeeking={setIsSeeking}
|
||||
repeat={repeat}
|
||||
setRepeat={setRepeat}
|
||||
setShowPoster={setShowPoster}
|
||||
setRate={setRate}
|
||||
setResizeMode={setResizeMode}
|
||||
setShowNotificationControls={setShowNotificationControls}
|
||||
showNotificationControls={showNotificationControls}
|
||||
setUseCache={setUseCache}
|
||||
setVolume={setVolume}
|
||||
useCache={useCache}
|
||||
srcListId={srcListId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default BasicExample;
|
240
examples/bare/src/BasicExample.windows.tsx
Normal file
240
examples/bare/src/BasicExample.windows.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import Video, {ResizeMode} from 'react-native-video';
|
||||
|
||||
class VideoPlayer extends Component {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.onLoad = this.onLoad.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: ResizeMode.CONTAIN,
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: false,
|
||||
};
|
||||
|
||||
onLoad(data: any) {
|
||||
this.setState({duration: data.duration});
|
||||
}
|
||||
|
||||
onProgress(data: any) {
|
||||
this.setState({currentTime: data.currentTime});
|
||||
}
|
||||
|
||||
getCurrentTimePercentage() {
|
||||
if (this.state.currentTime > 0 && this.state.duration !== 0) {
|
||||
return this.state.currentTime / this.state.duration;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
renderRateControl(rate: number) {
|
||||
const isSelected = this.state.rate === rate;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({rate: rate});
|
||||
}}>
|
||||
<Text style={style}>{rate}x</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderResizeModeControl(resizeMode: string) {
|
||||
const isSelected = this.state.resizeMode === resizeMode;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({resizeMode: resizeMode});
|
||||
}}>
|
||||
<Text style={style}>{resizeMode}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderVolumeControl(volume: number) {
|
||||
const isSelected = this.state.volume === volume;
|
||||
const style: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: isSelected ? 'bold' : 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.setState({volume: volume});
|
||||
}}>
|
||||
<Text style={style}>{volume * 100}%</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const flexCompleted = this.getCurrentTimePercentage() * 100;
|
||||
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity
|
||||
style={styles.fullScreen}
|
||||
onPress={() => {
|
||||
this.setState({paused: !this.state.paused});
|
||||
}}>
|
||||
<Video
|
||||
source={require('./assets/videos/broadchurch.mp4')}
|
||||
style={styles.fullScreen}
|
||||
rate={this.state.rate}
|
||||
paused={this.state.paused}
|
||||
volume={this.state.volume}
|
||||
muted={this.state.muted}
|
||||
resizeMode={this.state.resizeMode}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={() => {
|
||||
console.log('Done!');
|
||||
}}
|
||||
repeat={true}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.controls}>
|
||||
<View style={styles.generalControls}>
|
||||
<View style={styles.rateControl}>
|
||||
{this.renderRateControl(0.25)}
|
||||
{this.renderRateControl(0.5)}
|
||||
{this.renderRateControl(1.0)}
|
||||
{this.renderRateControl(1.5)}
|
||||
{this.renderRateControl(2.0)}
|
||||
</View>
|
||||
|
||||
<View style={styles.volumeControl}>
|
||||
{this.renderVolumeControl(0.5)}
|
||||
{this.renderVolumeControl(1)}
|
||||
{this.renderVolumeControl(1.5)}
|
||||
</View>
|
||||
|
||||
<View style={styles.resizeModeControl}>
|
||||
{this.renderResizeModeControl('cover')}
|
||||
{this.renderResizeModeControl('contain')}
|
||||
{this.renderResizeModeControl('stretch')}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.trackingControls}>
|
||||
<View style={styles.progress}>
|
||||
<View
|
||||
style={[styles.innerProgressCompleted, {flex: flexCompleted}]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.innerProgressRemaining, {flex: flexRemaining}]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
controls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
progress: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerProgressCompleted: {
|
||||
height: 20,
|
||||
backgroundColor: '#cccccc',
|
||||
},
|
||||
innerProgressRemaining: {
|
||||
height: 20,
|
||||
backgroundColor: '#2C2C2C',
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
trackingControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
export default VideoPlayer;
|
231
examples/bare/src/DRMExample.tsx
Normal file
231
examples/bare/src/DRMExample.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
Alert,
|
||||
Button,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import Video, {DRMType, ReactVideoSourceProperties} from 'react-native-video';
|
||||
|
||||
type SourceType = ReactVideoSourceProperties | null;
|
||||
|
||||
const DRMExample = () => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const [source, setSource] = React.useState<SourceType>(null);
|
||||
|
||||
const [hls, setHls] = React.useState(
|
||||
'https://d5lhbv70lxyop.cloudfront.net/02b91d1c-dcde-4a93-8391-8524f7836a72/assets/5a116d5e-4acb-4461-8bc0-81adf45a8432/videokit-576p-dash-hls-drm/hls/index.m3u8',
|
||||
);
|
||||
const [fairplayLicense, setFairplayLicense] = React.useState(
|
||||
'https://videokit-demo-7dr2zvpf.la.drm.cloud/acquire-license/fairplay?BrandGuid=02b91d1c-dcde-4a93-8391-8524f7836a72',
|
||||
);
|
||||
const [fairplayCertificate, setFairplayCertificate] = React.useState(
|
||||
'https://videokit-demo-7dr2zvpf.la.drm.cloud/certificate/fairplay?BrandGuid=02b91d1c-dcde-4a93-8391-8524f7836a72',
|
||||
);
|
||||
const [dash, setDash] = React.useState(
|
||||
'https://d5lhbv70lxyop.cloudfront.net/02b91d1c-dcde-4a93-8391-8524f7836a72/assets/5a116d5e-4acb-4461-8bc0-81adf45a8432/videokit-576p-dash-hls-drm/dash/index.mpd',
|
||||
);
|
||||
const [widevineLicense, setWidevineLicense] = React.useState(
|
||||
'https://videokit-demo-7dr2zvpf.la.drm.cloud/acquire-license/widevine?BrandGuid=02b91d1c-dcde-4a93-8391-8524f7836a72',
|
||||
);
|
||||
|
||||
// ------------- DMR Token -------------
|
||||
// This token is used to authenticate the user and get the license
|
||||
// To run example please go to https://someweb.com (TODO: Insert here real website cc Kamil) and complete the form to receive the token
|
||||
// After you receive the token, please paste it here
|
||||
const [token, setToken] = React.useState('<USER_TOKEN>');
|
||||
|
||||
const handlePlayStopVideo = () => {
|
||||
if (source !== null) {
|
||||
setSource(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token === '<USER_TOKEN>') {
|
||||
Alert.alert('Error', 'Please enter the token received from the website');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const newSource: ReactVideoSourceProperties = {};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
if (fairplayLicense && fairplayCertificate) {
|
||||
newSource.uri = hls;
|
||||
newSource.drm = {
|
||||
type: DRMType.FAIRPLAY,
|
||||
licenseServer: fairplayLicense,
|
||||
certificateUrl: fairplayCertificate,
|
||||
getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
|
||||
const formData = new FormData();
|
||||
formData.append('spc', spcString);
|
||||
|
||||
const resultURL = loadedLicenseUrl.replace('skd://', 'https://');
|
||||
|
||||
return fetch(`${resultURL}&userToken=${token}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
return response.ckc;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error', error);
|
||||
});
|
||||
},
|
||||
};
|
||||
} else {
|
||||
Alert.alert('Error', 'Please enter Fairplay License and Certificate');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (widevineLicense) {
|
||||
newSource.drm = {
|
||||
type: DRMType.WIDEVINE,
|
||||
licenseServer: widevineLicense,
|
||||
};
|
||||
newSource.uri = dash;
|
||||
} else {
|
||||
Alert.alert('Error', 'Please enter Widevine License');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
setSource(newSource);
|
||||
};
|
||||
|
||||
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>DRM is not supported on this platform</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<Text style={styles.title}>DRM Protected Stream Player</Text>
|
||||
|
||||
{loading && <ActivityIndicator size="large" color="#0000ff" />}
|
||||
{source && source.uri && (
|
||||
<Video
|
||||
key={source.uri}
|
||||
onLoad={() => {
|
||||
setLoading(false);
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.log('error', e);
|
||||
Alert.alert('Error', e.error.localizedDescription);
|
||||
setLoading(false);
|
||||
}}
|
||||
source={source}
|
||||
resizeMode="contain"
|
||||
style={styles.video}
|
||||
controls
|
||||
muted={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Platform.OS === 'ios' && (
|
||||
<>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="HLS URL"
|
||||
value={hls}
|
||||
onChangeText={(text) => setHls(text)}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Fairplay License URL"
|
||||
value={fairplayLicense}
|
||||
onChangeText={(text) => setFairplayLicense(text)}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Fairplay Certificate URL"
|
||||
value={fairplayCertificate}
|
||||
onChangeText={(text) => setFairplayCertificate(text)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{Platform.OS === 'android' && (
|
||||
<>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="DASH URL"
|
||||
value={dash}
|
||||
onChangeText={(text) => setDash(text)}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Widevine License URL"
|
||||
value={widevineLicense}
|
||||
onChangeText={(text) => setWidevineLicense(text)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Token"
|
||||
value={token}
|
||||
onChangeText={(text) => setToken(text)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={`${source !== null ? 'Stop' : 'Play'} Video`}
|
||||
onPress={handlePlayStopVideo}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default DRMExample;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 20,
|
||||
color: 'white',
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
marginBottom: 80,
|
||||
},
|
||||
input: {
|
||||
height: 40,
|
||||
borderColor: 'gray',
|
||||
borderWidth: 1,
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 10,
|
||||
width: '100%',
|
||||
color: 'white',
|
||||
},
|
||||
});
|
1
examples/bare/src/assets/index.ts
Normal file
1
examples/bare/src/assets/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './videos';
|
BIN
examples/bare/src/assets/videos/broadchurch.mp4
Normal file
BIN
examples/bare/src/assets/videos/broadchurch.mp4
Normal file
Binary file not shown.
4
examples/bare/src/assets/videos/index.ts
Normal file
4
examples/bare/src/assets/videos/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const localeVideo = {
|
||||
broadchurch: require('./broadchurch.mp4'),
|
||||
portrait: require('./portrait.mp4'),
|
||||
};
|
BIN
examples/bare/src/assets/videos/portrait.mp4
Normal file
BIN
examples/bare/src/assets/videos/portrait.mp4
Normal file
Binary file not shown.
65
examples/bare/src/components/AudioTracksSelector.tsx
Normal file
65
examples/bare/src/components/AudioTracksSelector.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
SelectedTrackType,
|
||||
type AudioTrack,
|
||||
type SelectedTrack,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface AudioTrackSelectorType {
|
||||
audioTracks: Array<AudioTrack>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string | number) => void;
|
||||
audioTracksSelectionBy: SelectedTrackType;
|
||||
}
|
||||
|
||||
export const AudioTrackSelector = ({
|
||||
audioTracks,
|
||||
selectedAudioTrack,
|
||||
onValueChange,
|
||||
audioTracksSelectionBy,
|
||||
}: AudioTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>AudioTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={selectedAudioTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
{audioTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{audioTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (audioTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = track.index;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = track.language;
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = track.title;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${value} - ${track.selected}`}
|
||||
value={`${value}`}
|
||||
key={`${value}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
8
examples/bare/src/components/Indicator.tsx
Normal file
8
examples/bare/src/components/Indicator.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React, {memo} from 'react';
|
||||
import {ActivityIndicator} from 'react-native';
|
||||
|
||||
const _Indicator = () => {
|
||||
return <ActivityIndicator color="#3235fd" size="large" />;
|
||||
};
|
||||
|
||||
export const Indicator = memo(_Indicator);
|
74
examples/bare/src/components/MultiValueControl.tsx
Normal file
74
examples/bare/src/components/MultiValueControl.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {ResizeMode} from 'react-native-video';
|
||||
|
||||
/*
|
||||
* MultiValueControl displays a list clickable text view
|
||||
*/
|
||||
|
||||
interface MultiValueControlType<T> {
|
||||
// a list a string or number to be displayed
|
||||
values: Array<T>;
|
||||
// The selected value in values
|
||||
selected?: T;
|
||||
// callback to press onPress
|
||||
onPress: (arg: T) => void;
|
||||
}
|
||||
|
||||
export const MultiValueControl = <T extends number | string | ResizeMode>({
|
||||
values,
|
||||
selected,
|
||||
onPress,
|
||||
}: MultiValueControlType<T>) => {
|
||||
const selectedStyle: TextStyle = StyleSheet.flatten([
|
||||
styles.option,
|
||||
{fontWeight: 'bold'},
|
||||
]);
|
||||
|
||||
const unselectedStyle: TextStyle = StyleSheet.flatten([
|
||||
styles.option,
|
||||
{fontWeight: 'normal'},
|
||||
]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{values.map(value => {
|
||||
const _style = value === selected ? selectedStyle : unselectedStyle;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={value}
|
||||
onPress={() => {
|
||||
onPress?.(value);
|
||||
}}>
|
||||
<Text style={_style}>{value}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
option: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default MultiValueControl;
|
350
examples/bare/src/components/Overlay.tsx
Normal file
350
examples/bare/src/components/Overlay.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import styles from '../styles.tsx';
|
||||
import {
|
||||
isAndroid,
|
||||
isIos,
|
||||
textTracksSelectionBy,
|
||||
audioTracksSelectionBy,
|
||||
} from '../constants';
|
||||
import {
|
||||
ResizeMode,
|
||||
VideoRef,
|
||||
SelectedTrackType,
|
||||
SelectedVideoTrackType,
|
||||
VideoDecoderProperties,
|
||||
type EnumValues,
|
||||
type TextTrack,
|
||||
type SelectedVideoTrack,
|
||||
type SelectedTrack,
|
||||
type VideoTrack,
|
||||
type AudioTrack,
|
||||
} from 'react-native-video';
|
||||
|
||||
import {toast} from './Toast';
|
||||
import {Seeker} from './Seeker';
|
||||
import {AudioTrackSelector} from './AudioTracksSelector';
|
||||
import {VideoTrackSelector} from './VideoTracksSelector';
|
||||
import {TextTrackSelector} from './TextTracksSelector';
|
||||
import {TopControl} from './TopControl';
|
||||
import {ToggleControl} from './ToggleControl';
|
||||
import {MultiValueControl} from './MultiValueControl';
|
||||
|
||||
type Props = {
|
||||
channelDown: () => void;
|
||||
channelUp: () => void;
|
||||
setFullscreen: Dispatch<SetStateAction<boolean>>;
|
||||
controls: boolean;
|
||||
setControls: Dispatch<SetStateAction<boolean>>;
|
||||
showNotificationControls: boolean;
|
||||
setShowNotificationControls: Dispatch<SetStateAction<boolean>>;
|
||||
selectedAudioTrack: SelectedTrack | undefined;
|
||||
setSelectedAudioTrack: Dispatch<SetStateAction<SelectedTrack | undefined>>;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
setSelectedTextTrack: (value: SelectedTrack | undefined) => void;
|
||||
selectedVideoTrack: SelectedVideoTrack;
|
||||
setSelectedVideoTrack: (value: SelectedVideoTrack) => void;
|
||||
setIsSeeking: Dispatch<SetStateAction<boolean>>;
|
||||
rate: number;
|
||||
setRate: Dispatch<SetStateAction<number>>;
|
||||
volume: number;
|
||||
setVolume: (value: number) => void;
|
||||
resizeMode: EnumValues<ResizeMode>;
|
||||
setResizeMode: Dispatch<SetStateAction<EnumValues<ResizeMode>>>;
|
||||
isLoading: boolean;
|
||||
srcListId: number;
|
||||
useCache: boolean;
|
||||
setUseCache: Dispatch<SetStateAction<boolean>>;
|
||||
paused: boolean;
|
||||
setPaused: Dispatch<SetStateAction<boolean>>;
|
||||
repeat: boolean;
|
||||
setRepeat: Dispatch<SetStateAction<boolean>>;
|
||||
showPoster: boolean;
|
||||
setShowPoster: Dispatch<SetStateAction<boolean>>;
|
||||
muted: boolean;
|
||||
setMuted: Dispatch<SetStateAction<boolean>>;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isSeeking: boolean;
|
||||
audioTracks: AudioTrack[];
|
||||
textTracks: TextTrack[];
|
||||
videoTracks: VideoTrack[];
|
||||
};
|
||||
|
||||
const _Overlay = forwardRef<VideoRef, Props>((props, ref) => {
|
||||
const {
|
||||
channelUp,
|
||||
channelDown,
|
||||
setFullscreen,
|
||||
setControls,
|
||||
controls,
|
||||
setShowNotificationControls,
|
||||
showNotificationControls,
|
||||
setSelectedAudioTrack,
|
||||
setSelectedTextTrack,
|
||||
setSelectedVideoTrack,
|
||||
setIsSeeking,
|
||||
rate,
|
||||
setRate,
|
||||
volume,
|
||||
setVolume,
|
||||
resizeMode,
|
||||
setResizeMode,
|
||||
isLoading,
|
||||
srcListId,
|
||||
setUseCache,
|
||||
useCache,
|
||||
paused,
|
||||
setPaused,
|
||||
setRepeat,
|
||||
repeat,
|
||||
setShowPoster,
|
||||
showPoster,
|
||||
setMuted,
|
||||
muted,
|
||||
duration,
|
||||
isSeeking,
|
||||
currentTime,
|
||||
textTracks,
|
||||
videoTracks,
|
||||
audioTracks,
|
||||
selectedAudioTrack,
|
||||
selectedVideoTrack,
|
||||
selectedTextTrack,
|
||||
} = props;
|
||||
const popupInfo = useCallback(() => {
|
||||
VideoDecoderProperties.getWidevineLevel().then((widevineLevel: number) => {
|
||||
VideoDecoderProperties.isHEVCSupported().then((hevc: string) => {
|
||||
VideoDecoderProperties.isCodecSupported('video/avc', 1920, 1080).then(
|
||||
(avc: string) => {
|
||||
toast(
|
||||
true,
|
||||
'Widevine level: ' +
|
||||
widevineLevel +
|
||||
'\n hevc: ' +
|
||||
hevc +
|
||||
'\n avc: ' +
|
||||
avc,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
setFullscreen(prev => !prev);
|
||||
};
|
||||
const toggleControls = () => {
|
||||
setControls(prev => !prev);
|
||||
};
|
||||
|
||||
const openDecoration = () => {
|
||||
typeof ref !== 'function' && ref?.current?.setFullScreen(true);
|
||||
};
|
||||
|
||||
const toggleShowNotificationControls = () => {
|
||||
setShowNotificationControls(prev => !prev);
|
||||
};
|
||||
|
||||
const onSelectedAudioTrackChange = (itemValue: string | number) => {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
if (itemValue === 'none') {
|
||||
setSelectedAudioTrack({
|
||||
type: SelectedTrackType.DISABLED,
|
||||
});
|
||||
} else {
|
||||
setSelectedAudioTrack({type: audioTracksSelectionBy, value: itemValue});
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedTextTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
setSelectedTextTrack({type: textTracksSelectionBy, value: itemValue});
|
||||
};
|
||||
|
||||
const onSelectedVideoTrackChange = (itemValue: string) => {
|
||||
console.log('on value change ' + itemValue);
|
||||
if (itemValue === undefined || itemValue === 'auto') {
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.AUTO,
|
||||
});
|
||||
} else {
|
||||
setSelectedVideoTrack({
|
||||
type: SelectedVideoTrackType.INDEX,
|
||||
value: itemValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const videoSeek = (position: number) => {
|
||||
setIsSeeking(true);
|
||||
typeof ref !== 'function' && ref?.current?.seek(position);
|
||||
};
|
||||
|
||||
const onRateSelected = (value: number) => {
|
||||
setRate(value);
|
||||
};
|
||||
|
||||
const onVolumeSelected = (value: number) => {
|
||||
setVolume(value);
|
||||
};
|
||||
|
||||
const onResizeModeSelected = (value: EnumValues<ResizeMode>) => {
|
||||
setResizeMode(value);
|
||||
};
|
||||
|
||||
const toggleCache = () => setUseCache(prev => !prev);
|
||||
|
||||
const togglePause = () => setPaused(prev => !prev);
|
||||
|
||||
const toggleRepeat = () => setRepeat(prev => !prev);
|
||||
|
||||
const togglePoster = () => setShowPoster(prev => !prev);
|
||||
|
||||
const toggleMuted = () => setMuted(prev => !prev);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.topControls}>
|
||||
<View style={styles.resizeModeControl}>
|
||||
<TopControl
|
||||
srcListId={srcListId}
|
||||
showRNVControls={controls}
|
||||
toggleControls={toggleControls}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{!controls ? (
|
||||
<>
|
||||
<View style={styles.leftControls}>
|
||||
<ToggleControl onPress={channelDown} text="ChDown" />
|
||||
</View>
|
||||
<View style={styles.rightControls}>
|
||||
<ToggleControl onPress={channelUp} text="ChUp" />
|
||||
</View>
|
||||
<View style={styles.bottomControls}>
|
||||
<View style={styles.generalControls}>
|
||||
{isAndroid ? (
|
||||
<View style={styles.generalControls}>
|
||||
<ToggleControl onPress={popupInfo} text="decoderInfo" />
|
||||
<ToggleControl
|
||||
isSelected={useCache}
|
||||
onPress={toggleCache}
|
||||
selectedText="enable cache"
|
||||
unselectedText="disable cache"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<ToggleControl
|
||||
isSelected={paused}
|
||||
onPress={togglePause}
|
||||
selectedText="pause"
|
||||
unselectedText="playing"
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={repeat}
|
||||
onPress={toggleRepeat}
|
||||
selectedText="loop enable"
|
||||
unselectedText="loop disable"
|
||||
/>
|
||||
<ToggleControl onPress={toggleFullscreen} text="fullscreen" />
|
||||
<ToggleControl onPress={openDecoration} text="decoration" />
|
||||
<ToggleControl
|
||||
isSelected={showPoster}
|
||||
onPress={togglePoster}
|
||||
selectedText="poster"
|
||||
unselectedText="no poster"
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={showNotificationControls}
|
||||
onPress={toggleShowNotificationControls}
|
||||
selectedText="hide notification controls"
|
||||
unselectedText="show notification controls"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.generalControls}>
|
||||
{/* shall be replaced by slider */}
|
||||
<MultiValueControl
|
||||
values={[0, 0.25, 0.5, 1.0, 1.5, 2.0]}
|
||||
onPress={onRateSelected}
|
||||
selected={rate}
|
||||
/>
|
||||
{/* shall be replaced by slider */}
|
||||
<MultiValueControl
|
||||
values={[0.5, 1, 1.5]}
|
||||
onPress={onVolumeSelected}
|
||||
selected={volume}
|
||||
/>
|
||||
<MultiValueControl
|
||||
values={[
|
||||
ResizeMode.COVER,
|
||||
ResizeMode.CONTAIN,
|
||||
ResizeMode.STRETCH,
|
||||
]}
|
||||
onPress={onResizeModeSelected}
|
||||
selected={resizeMode}
|
||||
/>
|
||||
<ToggleControl
|
||||
isSelected={muted}
|
||||
onPress={toggleMuted}
|
||||
text="muted"
|
||||
/>
|
||||
{isIos ? (
|
||||
<ToggleControl
|
||||
isSelected={paused}
|
||||
onPress={() => {
|
||||
typeof ref !== 'function' &&
|
||||
ref?.current
|
||||
?.save({})
|
||||
?.then((response: unknown) => {
|
||||
console.log('Downloaded URI', response);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.log('error during save ', error);
|
||||
});
|
||||
}}
|
||||
text="save"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<Seeker
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isLoading={isLoading}
|
||||
videoSeek={prop => videoSeek(prop)}
|
||||
isUISeeking={isSeeking}
|
||||
/>
|
||||
<View style={styles.generalControls}>
|
||||
<AudioTrackSelector
|
||||
audioTracks={audioTracks}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
onValueChange={onSelectedAudioTrackChange}
|
||||
audioTracksSelectionBy={audioTracksSelectionBy}
|
||||
/>
|
||||
<TextTrackSelector
|
||||
textTracks={textTracks}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
onValueChange={onSelectedTextTrackChange}
|
||||
textTracksSelectionBy={textTracksSelectionBy}
|
||||
/>
|
||||
<VideoTrackSelector
|
||||
videoTracks={videoTracks}
|
||||
selectedVideoTrack={selectedVideoTrack}
|
||||
onValueChange={onSelectedVideoTrackChange}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const Overlay = memo(_Overlay);
|
152
examples/bare/src/components/Seeker.tsx
Normal file
152
examples/bare/src/components/Seeker.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {PanResponder, View} from 'react-native';
|
||||
import styles from '../styles';
|
||||
|
||||
interface SeekerProps {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isLoading: boolean;
|
||||
isUISeeking: boolean;
|
||||
videoSeek: (arg0: number) => void;
|
||||
}
|
||||
|
||||
export const Seeker = ({
|
||||
currentTime,
|
||||
duration,
|
||||
isLoading,
|
||||
isUISeeking,
|
||||
videoSeek,
|
||||
}: SeekerProps) => {
|
||||
const [seeking, setSeeking] = useState(false);
|
||||
const [seekerPosition, setSeekerPosition] = useState(0);
|
||||
const [seekerWidth, setSeekerWidth] = useState(0);
|
||||
|
||||
/**
|
||||
* Set the position of the seekbar's components
|
||||
* (both fill and handle) according to the
|
||||
* position supplied.
|
||||
*
|
||||
* @param {float} position position in px of seeker handle}
|
||||
*/
|
||||
const updateSeekerPosition = useCallback(
|
||||
(position = 0) => {
|
||||
if (position <= 0) {
|
||||
position = 0;
|
||||
} else if (position >= seekerWidth) {
|
||||
position = seekerWidth;
|
||||
}
|
||||
setSeekerPosition(position);
|
||||
},
|
||||
[seekerWidth],
|
||||
);
|
||||
|
||||
/**
|
||||
* Return the time that the video should be at
|
||||
* based on where the seeker handle is.
|
||||
*
|
||||
* @return {float} time in ms based on seekerPosition.
|
||||
*/
|
||||
const calculateTimeFromSeekerPosition = () => {
|
||||
const percent = seekerPosition / seekerWidth;
|
||||
return duration * percent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get our seekbar responder going
|
||||
*/
|
||||
|
||||
const seekPanResponder = PanResponder.create({
|
||||
// Ask to be the responder.
|
||||
onStartShouldSetPanResponder: (_evt, _gestureState) => true,
|
||||
onMoveShouldSetPanResponder: (_evt, _gestureState) => true,
|
||||
|
||||
/**
|
||||
* When we start the pan tell the machine that we're
|
||||
* seeking. This stops it from updating the seekbar
|
||||
* position in the onProgress listener.
|
||||
*/
|
||||
onPanResponderGrant: (evt, _gestureState) => {
|
||||
const position = evt.nativeEvent.locationX;
|
||||
updateSeekerPosition(position);
|
||||
setSeeking(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* When panning, update the seekbar position, duh.
|
||||
*/
|
||||
onPanResponderMove: (evt, _gestureState) => {
|
||||
const position = evt.nativeEvent.locationX;
|
||||
updateSeekerPosition(position);
|
||||
},
|
||||
|
||||
/**
|
||||
* On release we update the time and seek to it in the video.
|
||||
* If you seek to the end of the video we fire the
|
||||
* onEnd callback
|
||||
*/
|
||||
onPanResponderRelease: (_evt, _gestureState) => {
|
||||
const time = calculateTimeFromSeekerPosition();
|
||||
if (time >= duration && !isLoading) {
|
||||
// FIXME ...
|
||||
// state.paused = true;
|
||||
// this.onEnd();
|
||||
} else {
|
||||
videoSeek(time);
|
||||
setSeeking(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !seeking && !isUISeeking) {
|
||||
const percent = currentTime / duration;
|
||||
const position = seekerWidth * percent;
|
||||
updateSeekerPosition(position);
|
||||
}
|
||||
}, [
|
||||
currentTime,
|
||||
duration,
|
||||
isLoading,
|
||||
seekerWidth,
|
||||
seeking,
|
||||
isUISeeking,
|
||||
updateSeekerPosition,
|
||||
]);
|
||||
|
||||
if (!seekPanResponder) {
|
||||
return null;
|
||||
}
|
||||
const seekerStyle = [
|
||||
styles.seekbarFill,
|
||||
{
|
||||
width: seekerPosition > 0 ? seekerPosition : 0,
|
||||
backgroundColor: '#FFF',
|
||||
},
|
||||
];
|
||||
|
||||
const seekerPositionStyle = [
|
||||
styles.seekbarHandle,
|
||||
{
|
||||
left: seekerPosition > 0 ? seekerPosition : 0,
|
||||
},
|
||||
];
|
||||
|
||||
const seekerPointerStyle = [styles.seekbarCircle, {backgroundColor: '#FFF'}];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.seekbarContainer}
|
||||
{...seekPanResponder.panHandlers}
|
||||
{...styles.generalControls}>
|
||||
<View
|
||||
style={styles.seekbarTrack}
|
||||
onLayout={event => setSeekerWidth(event.nativeEvent.layout.width)}
|
||||
pointerEvents={'none'}>
|
||||
<View style={seekerStyle} pointerEvents={'none'} />
|
||||
</View>
|
||||
<View style={seekerPositionStyle} pointerEvents={'none'}>
|
||||
<View style={seekerPointerStyle} pointerEvents={'none'} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
58
examples/bare/src/components/TextTracksSelector.tsx
Normal file
58
examples/bare/src/components/TextTracksSelector.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
type TextTrack,
|
||||
type SelectedTrack,
|
||||
SelectedTrackType,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface TextTrackSelectorType {
|
||||
textTracks: Array<TextTrack>;
|
||||
selectedTextTrack: SelectedTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
textTracksSelectionBy: string;
|
||||
}
|
||||
|
||||
export const TextTrackSelector = ({
|
||||
textTracks,
|
||||
selectedTextTrack,
|
||||
onValueChange,
|
||||
textTracksSelectionBy,
|
||||
}: TextTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>TextTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={`${selectedTextTrack?.value}`}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
{textTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{textTracks.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = track.index;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.LANGUAGE) {
|
||||
value = track.language;
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = track.title;
|
||||
}
|
||||
return <Picker.Item label={`${value}`} value={value} key={value} />;
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
18
examples/bare/src/components/Toast.ts
Normal file
18
examples/bare/src/components/Toast.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {Alert, ToastAndroid} from 'react-native';
|
||||
import {isAndroid} from '../constants';
|
||||
|
||||
export const toast = (visible: boolean, message: string) => {
|
||||
if (visible) {
|
||||
if (isAndroid) {
|
||||
ToastAndroid.showWithGravityAndOffset(
|
||||
message,
|
||||
ToastAndroid.LONG,
|
||||
ToastAndroid.BOTTOM,
|
||||
25,
|
||||
50,
|
||||
);
|
||||
} else {
|
||||
Alert.alert(message, message);
|
||||
}
|
||||
}
|
||||
};
|
73
examples/bare/src/components/ToggleControl.tsx
Normal file
73
examples/bare/src/components/ToggleControl.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
/*
|
||||
* ToggleControl displays a 2 states clickable text
|
||||
*/
|
||||
|
||||
interface ToggleControlType {
|
||||
// boolean indicating if text is selected state
|
||||
isSelected?: boolean;
|
||||
// value of text when selected
|
||||
selectedText?: string;
|
||||
// value of text when NOT selected
|
||||
unselectedText?: string;
|
||||
// default text if no only one text field is needed
|
||||
text?: string;
|
||||
// callback called when pressing the component
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleControl = ({
|
||||
isSelected,
|
||||
selectedText,
|
||||
unselectedText,
|
||||
text,
|
||||
onPress,
|
||||
}: ToggleControlType) => {
|
||||
const selectedStyle: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: 'bold'},
|
||||
]);
|
||||
|
||||
const unselectedStyle: TextStyle = StyleSheet.flatten([
|
||||
styles.controlOption,
|
||||
{fontWeight: 'normal'},
|
||||
]);
|
||||
|
||||
const style = isSelected ? selectedStyle : unselectedStyle;
|
||||
const _text = text ? text : isSelected ? selectedText : unselectedText;
|
||||
return (
|
||||
<View style={styles.resizeModeControl}>
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<Text style={style}>{_text}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default ToggleControl;
|
37
examples/bare/src/components/TopControl.tsx
Normal file
37
examples/bare/src/components/TopControl.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, {FC, memo} from 'react';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
import styles from '../styles.tsx';
|
||||
import {srcList} from '../constants';
|
||||
import {type AdditionalSourceInfo} from '../types';
|
||||
|
||||
type Props = {
|
||||
srcListId: number;
|
||||
showRNVControls: boolean;
|
||||
toggleControls: () => void;
|
||||
};
|
||||
|
||||
const _TopControl: FC<Props> = ({
|
||||
toggleControls,
|
||||
showRNVControls,
|
||||
srcListId,
|
||||
}) => {
|
||||
return (
|
||||
<View style={styles.topControlsContainer}>
|
||||
<Text style={styles.controlOption}>
|
||||
{(srcList[srcListId] as AdditionalSourceInfo)?.description ||
|
||||
'local file'}
|
||||
</Text>
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
toggleControls();
|
||||
}}>
|
||||
<Text style={styles.leftRightControlOption}>
|
||||
{showRNVControls ? 'Hide controls' : 'Show controls'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export const TopControl = memo(_TopControl);
|
15
examples/bare/src/components/VideoLoader.tsx
Normal file
15
examples/bare/src/components/VideoLoader.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import {Text, View} from 'react-native';
|
||||
import {Indicator} from './Indicator.tsx';
|
||||
import React, {memo} from 'react';
|
||||
import styles from '../styles.tsx';
|
||||
|
||||
const _VideoLoader = () => {
|
||||
return (
|
||||
<View style={styles.indicatorContainer}>
|
||||
<Text style={styles.indicatorText}>Loading...</Text>
|
||||
<Indicator />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const VideoLoader = memo(_VideoLoader);
|
62
examples/bare/src/components/VideoTracksSelector.tsx
Normal file
62
examples/bare/src/components/VideoTracksSelector.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {Picker} from '@react-native-picker/picker';
|
||||
import {Text} from 'react-native';
|
||||
import {
|
||||
SelectedVideoTrackType,
|
||||
type SelectedVideoTrack,
|
||||
type VideoTrack,
|
||||
} from 'react-native-video';
|
||||
import styles from '../styles';
|
||||
import React from 'react';
|
||||
|
||||
export interface VideoTrackSelectorType {
|
||||
videoTracks: Array<VideoTrack>;
|
||||
selectedVideoTrack: SelectedVideoTrack | undefined;
|
||||
onValueChange: (arg0: string) => void;
|
||||
}
|
||||
|
||||
export const VideoTrackSelector = ({
|
||||
videoTracks,
|
||||
selectedVideoTrack,
|
||||
onValueChange,
|
||||
}: VideoTrackSelectorType) => {
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.controlOption}>VideoTrack</Text>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={
|
||||
selectedVideoTrack === undefined ||
|
||||
selectedVideoTrack?.type === SelectedVideoTrackType.AUTO
|
||||
? 'auto'
|
||||
: `${selectedVideoTrack?.value}`
|
||||
}
|
||||
onValueChange={itemValue => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
}}>
|
||||
<Picker.Item label={'auto'} value={'auto'} key={'auto'} />
|
||||
{videoTracks?.length <= 0 || videoTracks?.length <= 0 ? (
|
||||
<Picker.Item label={'empty'} value={'empty'} key={'empty'} />
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{videoTracks?.map(track => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${track.width}x${track.height} ${Math.floor(
|
||||
(track.bitrate || 0) / 8 / 1024,
|
||||
)} Kbps`}
|
||||
value={`${track.index}`}
|
||||
key={track.index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
);
|
||||
};
|
11
examples/bare/src/components/index.ts
Normal file
11
examples/bare/src/components/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './VideoLoader';
|
||||
export * from './Indicator';
|
||||
export * from './Seeker';
|
||||
export * from './AudioTracksSelector';
|
||||
export * from './VideoTracksSelector';
|
||||
export * from './TextTracksSelector';
|
||||
export * from './Overlay';
|
||||
export * from './TopControl';
|
||||
export * from './Toast';
|
||||
export * from './ToggleControl';
|
||||
export * from './MultiValueControl';
|
184
examples/bare/src/constants/general.ts
Normal file
184
examples/bare/src/constants/general.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
BufferConfig,
|
||||
DRMType,
|
||||
ISO639_1,
|
||||
SelectedTrackType,
|
||||
TextTrackType,
|
||||
} from 'react-native-video';
|
||||
import {SampleVideoSource} from '../types';
|
||||
import {localeVideo} from '../assets';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
// This constant allows to change how the sample behaves regarding to audio and texts selection.
|
||||
// You can change it to change how selector will use tracks information.
|
||||
// by default, index will be displayed and index will be applied to selected tracks.
|
||||
// You can also use LANGUAGE or TITLE
|
||||
export const textTracksSelectionBy = SelectedTrackType.INDEX;
|
||||
export const audioTracksSelectionBy = SelectedTrackType.INDEX;
|
||||
|
||||
export const isIos = Platform.OS === 'ios';
|
||||
|
||||
export const isAndroid = Platform.OS === 'android';
|
||||
|
||||
export const srcAllPlatformList = [
|
||||
{
|
||||
description: 'local file landscape',
|
||||
uri: localeVideo.broadchurch,
|
||||
},
|
||||
{
|
||||
description: 'local file landscape cropped',
|
||||
uri: localeVideo.broadchurch,
|
||||
cropStart: 3000,
|
||||
cropEnd: 10000,
|
||||
},
|
||||
{
|
||||
description: 'video with 90° rotation',
|
||||
uri: 'https://bn-dev.fra1.digitaloceanspaces.com/km-tournament/uploads/rn_image_picker_lib_temp_2ee86a27_9312_4548_84af_7fd75d9ad4dd_ad8b20587a.mp4',
|
||||
},
|
||||
{
|
||||
description: 'local file portrait',
|
||||
uri: localeVideo.portrait,
|
||||
metadata: {
|
||||
title: 'Test Title',
|
||||
subtitle: 'Test Subtitle',
|
||||
artist: 'Test Artist',
|
||||
description: 'Test Description',
|
||||
imageUri:
|
||||
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: '(hls|live) red bull tv',
|
||||
textTracksAllowChunklessPreparation: false,
|
||||
uri: 'https://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_928.m3u8',
|
||||
metadata: {
|
||||
title: 'Custom Title',
|
||||
subtitle: 'Custom Subtitle',
|
||||
artist: 'Custom Artist',
|
||||
description: 'Custom Description',
|
||||
imageUri:
|
||||
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'invalid URL',
|
||||
uri: 'mmt://www.youtube.com',
|
||||
type: 'mpd',
|
||||
},
|
||||
{description: '(no url) Stopped playback', uri: undefined},
|
||||
{
|
||||
description: '(no view) no View',
|
||||
noView: true,
|
||||
},
|
||||
{
|
||||
description: 'Another live sample',
|
||||
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
|
||||
},
|
||||
{
|
||||
description: 'another bunny (can be saved)',
|
||||
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
|
||||
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
|
||||
},
|
||||
{
|
||||
description: 'sintel with subtitles',
|
||||
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
},
|
||||
{
|
||||
description: 'sintel starts at 20sec',
|
||||
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
startPosition: 50000,
|
||||
},
|
||||
{
|
||||
description: 'mp3 with texttrack',
|
||||
uri: 'https://traffic.libsyn.com/democracynow/wx2024-0702_SOT_DeadCalm-LucileSmith-FULL-V2.mxf-audio.mp3', // an mp3 file
|
||||
textTracks: [], // empty text track list
|
||||
},
|
||||
{
|
||||
description: 'BigBugBunny sideLoaded subtitles',
|
||||
// sideloaded subtitles wont work for streaming like HLS on ios
|
||||
// mp4
|
||||
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
|
||||
textTracks: [
|
||||
{
|
||||
title: 'test',
|
||||
language: 'en' as ISO639_1,
|
||||
type: TextTrackType.VTT,
|
||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description: '(mp4) big buck bunny With Ads',
|
||||
ad: {
|
||||
adTagUrl:
|
||||
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
|
||||
},
|
||||
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
|
||||
},
|
||||
];
|
||||
|
||||
export const srcIosList: SampleVideoSource[] = [];
|
||||
|
||||
export const srcAndroidList: SampleVideoSource[] = [
|
||||
{
|
||||
description: 'Another live sample',
|
||||
uri: 'https://live.forstreet.cl/live/livestream.m3u8',
|
||||
},
|
||||
{
|
||||
description: 'asset file',
|
||||
uri: 'asset:///broadchurch.mp4',
|
||||
},
|
||||
{
|
||||
description: '(dash) sintel subtitles',
|
||||
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/sintel.mpd',
|
||||
},
|
||||
{
|
||||
description: '(mp4) big buck bunny',
|
||||
uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
|
||||
},
|
||||
{
|
||||
description: '(mp4|subtitles) demo with sintel Subtitles',
|
||||
uri: 'http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0',
|
||||
type: 'mpd',
|
||||
},
|
||||
{
|
||||
description: 'WV: Secure SD & HD (cbcs,MP4,H264)',
|
||||
uri: 'https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd',
|
||||
drm: {
|
||||
type: DRMType.WIDEVINE,
|
||||
licenseServer:
|
||||
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Secure UHD (cenc)',
|
||||
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd',
|
||||
drm: {
|
||||
type: DRMType.WIDEVINE,
|
||||
licenseServer:
|
||||
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'rtsp big bug bunny',
|
||||
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
|
||||
type: 'rtsp',
|
||||
},
|
||||
];
|
||||
|
||||
const platformSrc: SampleVideoSource[] = isAndroid
|
||||
? srcAndroidList
|
||||
: srcIosList;
|
||||
|
||||
export const srcList: SampleVideoSource[] =
|
||||
platformSrc.concat(srcAllPlatformList);
|
||||
|
||||
export const bufferConfig: BufferConfig = {
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 50000,
|
||||
bufferForPlaybackMs: 2500,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
live: {
|
||||
targetOffsetMs: 500,
|
||||
},
|
||||
};
|
1
examples/bare/src/constants/index.ts
Normal file
1
examples/bare/src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './general';
|
173
examples/bare/src/styles.tsx
Normal file
173
examples/bare/src/styles.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
halfScreen: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
left: 50,
|
||||
bottom: 100,
|
||||
right: 100,
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
bottomControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
leftControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
},
|
||||
rightControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
},
|
||||
topControls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftRightControlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
padding: 10,
|
||||
lineHeight: 12,
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
pickerContainer: {
|
||||
width: 100,
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
borderWidth: 1,
|
||||
borderColor: 'red',
|
||||
},
|
||||
indicatorContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
indicatorText: {
|
||||
color: 'white',
|
||||
},
|
||||
seekbarContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
height: 30,
|
||||
},
|
||||
seekbarTrack: {
|
||||
backgroundColor: '#333',
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
top: 14,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarFill: {
|
||||
backgroundColor: '#FFF',
|
||||
height: 1,
|
||||
width: '100%',
|
||||
},
|
||||
seekbarHandle: {
|
||||
position: 'absolute',
|
||||
marginLeft: -7,
|
||||
height: 28,
|
||||
width: 28,
|
||||
},
|
||||
seekbarCircle: {
|
||||
borderRadius: 12,
|
||||
position: 'relative',
|
||||
top: 8,
|
||||
left: 8,
|
||||
height: 12,
|
||||
width: 12,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
color: 'white',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
pickerItem: {
|
||||
color: 'white',
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
emptyPickerItem: {
|
||||
color: 'white',
|
||||
marginTop: 20,
|
||||
marginLeft: 20,
|
||||
flex: 1,
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
topControlsContainer: {
|
||||
paddingTop: 30,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
1
examples/bare/src/types/index.ts
Normal file
1
examples/bare/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types';
|
11
examples/bare/src/types/types.ts
Normal file
11
examples/bare/src/types/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Drm, ReactVideoSource, TextTracks} from 'react-native-video';
|
||||
|
||||
export type AdditionalSourceInfo = {
|
||||
textTracks?: TextTracks;
|
||||
adTagUrl?: string;
|
||||
description?: string;
|
||||
drm?: Drm;
|
||||
noView?: boolean;
|
||||
};
|
||||
|
||||
export type SampleVideoSource = ReactVideoSource | AdditionalSourceInfo;
|
3
examples/bare/tsconfig.json
Normal file
3
examples/bare/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@react-native/typescript-config/tsconfig.json"
|
||||
}
|
9
examples/bare/visionos/Podfile
Normal file
9
examples/bare/visionos/Podfile
Normal file
@@ -0,0 +1,9 @@
|
||||
ws_dir = Pathname.new(__dir__)
|
||||
ws_dir = ws_dir.parent until
|
||||
File.exist?("#{ws_dir}/node_modules/react-native-test-app/visionos/test_app.rb") ||
|
||||
ws_dir.expand_path.to_s == '/'
|
||||
require "#{ws_dir}/node_modules/react-native-test-app/visionos/test_app.rb"
|
||||
|
||||
workspace 'BareExample.xcworkspace'
|
||||
|
||||
use_test_app!
|
1333
examples/bare/visionos/Podfile.lock
Normal file
1333
examples/bare/visionos/Podfile.lock
Normal file
File diff suppressed because it is too large
Load Diff
33
examples/bare/windows/.gitignore
vendored
Normal file
33
examples/bare/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
.vs/
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
ARM64/
|
||||
AppPackages/
|
||||
[Bb]in/
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Oo]bj/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
bld/
|
||||
build/
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# NuGet Packages Directory
|
||||
packages/
|
||||
|
||||
**/Generated Files/**
|
||||
*.binlog
|
||||
*.hprof
|
||||
*.sln
|
||||
ExperimentalFeatures.props
|
||||
NuGet.Config
|
||||
dist/
|
||||
msbuild.binlog
|
||||
node_modules/
|
Reference in New Issue
Block a user