├── app.plugin.js
├── android
├── src
│ └── main
│ │ ├── res
│ │ └── values
│ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── yuanzhou
│ │ └── vlc
│ │ ├── ReactVlcPlayerPackage.java
│ │ └── vlcplayer
│ │ ├── VideoEventEmitter.java
│ │ ├── ReactVlcPlayerViewManager.java
│ │ └── ReactVlcPlayerView.java
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── proguard-rules.pro
├── build.gradle
└── react-native-yz-vlcplayer.iml
├── ios
├── RCTVLCPlayer
│ ├── RCTVLCPlayerManager.h
│ ├── RCTVLCPlayer.h
│ ├── RCTVLCPlayerManager.m
│ └── RCTVLCPlayer.m
└── RCTVLCPlayer.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── admin.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── xcuserdata
│ ├── aolc.xcuserdatad
│ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ └── admin.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── project.pbxproj
├── index.js
├── react-native-vlc-media-player.podspec
├── .github
└── workflows
│ ├── issues.yml
│ ├── npmpublish.yml
│ └── npmpublish-beta.yml
├── expo
├── withVlcMediaPlayer.js
├── ios
│ └── withMobileVlcKit.js
└── android
│ └── withGradleTasks.js
├── LICENSE
├── package.json
├── playerView
├── BackHandle.js
├── TimeLimit.js
├── SizeController.js
├── ControlBtn.js
├── index.js
└── VLCPlayerView.js
├── .gitignore
├── index.d.ts
├── VLCPlayer.js
└── README.md
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./expo/withVlcMediaPlayer");
2 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | vlcplayer
3 |
4 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer/RCTVLCPlayerManager.h:
--------------------------------------------------------------------------------
1 | #import "React/RCTViewManager.h"
2 |
3 | @interface RCTVLCPlayerManager : RCTViewManager
4 |
5 | @end
6 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razorRun/react-native-vlc-media-player/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 | const VLCPlayerControl = {
3 | VLCPlayer: require('./VLCPlayer').default,
4 | VlCPlayerView: require('./playerView/index').default,
5 | }
6 |
7 | module.exports = VLCPlayerControl;
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razorRun/react-native-vlc-media-player/HEAD/ios/RCTVLCPlayer.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/xcuserdata/aolc.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | RCTVLCPlayer.xcscheme
8 |
9 | orderHint
10 | 52
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | RCTVLCPlayer.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 | RCTVLCPlayer.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/react-native-vlc-media-player.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "react-native-vlc-media-player"
3 | s.version = "1.0.38"
4 | s.summary = "VLC player"
5 | s.requires_arc = true
6 | s.author = { 'roshan.milinda' => 'rmilinda@gmail.com' }
7 | s.license = 'MIT'
8 | s.homepage = 'https://github.com/razorRun/react-native-vlc-media-player.git'
9 | s.source = { :git => "https://github.com/razorRun/react-native-vlc-media-player.git" }
10 | s.source_files = 'ios/RCTVLCPlayer/*'
11 | s.ios.deployment_target = "8.4"
12 | s.tvos.deployment_target = "10.2"
13 | s.static_framework = true
14 | s.dependency 'React'
15 | s.ios.dependency 'MobileVLCKit', '3.5.1'
16 | s.tvos.dependency 'TVVLCKit', '3.5.1'
17 | end
18 |
--------------------------------------------------------------------------------
/.github/workflows/issues.yml:
--------------------------------------------------------------------------------
1 | name: Close inactive issues
2 | on:
3 | schedule:
4 | - cron: "30 1 * * *"
5 |
6 | jobs:
7 | close-issues:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 | steps:
13 | - uses: actions/stale@v9
14 | with:
15 | days-before-issue-stale: 60
16 | days-before-issue-close: 14
17 | stale-issue-label: "stale"
18 | stale-issue-message: "This issue is stale because it has been open for 60 days with no activity."
19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
20 | days-before-pr-stale: -1
21 | days-before-pr-close: -1
22 | repo-token: ${{ secrets.GITHUB_TOKEN }}
23 |
--------------------------------------------------------------------------------
/expo/withVlcMediaPlayer.js:
--------------------------------------------------------------------------------
1 | const withGradleTasks = require("./android/withGradleTasks");
2 | const withMobileVlcKit = require("./ios/withMobileVlcKit");
3 |
4 | /**
5 | * Adds required native code to work with expo development build
6 | *
7 | * @param {object} config - Expo native configuration
8 | * @param {object} options - Plugin options
9 | * @param {boolean} options.ios.includeVLCKit - If `true`, it will include VLC Kit on PodFile (No need if you are running RN 0.61 and up)
10 | * @param {boolean} options.android.legacyJetifier - Must be `true`, if react-native version lower than 0.71 to replace jetifier name on from react native libs
11 | *
12 | * @returns resolved expo configuration
13 | */
14 | const withVlcMediaPlayer = (config, options) => {
15 | config = withGradleTasks(config, options);
16 | config = withMobileVlcKit(config, options);
17 |
18 | return config;
19 | };
20 |
21 | module.exports = withVlcMediaPlayer;
22 |
--------------------------------------------------------------------------------
/android/src/main/java/com/yuanzhou/vlc/ReactVlcPlayerPackage.java:
--------------------------------------------------------------------------------
1 | package com.yuanzhou.vlc;
2 |
3 |
4 | import com.facebook.react.ReactPackage;
5 | import com.facebook.react.bridge.JavaScriptModule;
6 | import com.facebook.react.bridge.NativeModule;
7 | import com.facebook.react.bridge.ReactApplicationContext;
8 | import com.facebook.react.uimanager.ViewManager;
9 |
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 | import com.yuanzhou.vlc.vlcplayer.ReactVlcPlayerViewManager;
14 |
15 | public class ReactVlcPlayerPackage implements ReactPackage {
16 |
17 | @Override
18 | public List createNativeModules(ReactApplicationContext reactContext) {
19 | return Collections.emptyList();
20 | }
21 |
22 | // Deprecated RN 0.47
23 | public List> createJSModules() {
24 | return Collections.emptyList();
25 | }
26 |
27 | @Override
28 | public List createViewManagers(ReactApplicationContext reactContext) {
29 | return Collections.singletonList(new ReactVlcPlayerViewManager());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Brent Vatne, Baris Sencan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/aolc/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
27 | # https://github.com/pedroSG94/vlc-example-streamplayer/issues/28
28 |
29 | -keep class org.videolan.libvlc.** { *; }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-vlc-media-player",
3 | "version": "1.0.98",
4 | "description": "React native media player for video streaming and playing. Supports RTSP,RTMP and other protocols supported by VLC player",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "vlc",
11 | "player",
12 | "android",
13 | "ios",
14 | "react-native",
15 | "mp4",
16 | "rtsp",
17 | "media",
18 | "video"
19 | ],
20 | "author": "rmilinda@gmail.com",
21 | "dependencies": {
22 | "react-native-slider": "^0.11.0",
23 | "react-native-vector-icons": "^9.2.0"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/razorRun/react-native-vlc-media-player.git"
28 | },
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/razorRun/react-native-vlc-media-player/issues"
32 | },
33 | "homepage": "https://github.com/razorRun/react-native-vlc-media-player#readme",
34 | "devDependencies": {
35 | "@expo/config-plugins": "^7.2.2",
36 | "@types/react": "~18.2.14"
37 | },
38 | "typings": "index.d.ts"
39 | }
40 |
--------------------------------------------------------------------------------
/expo/ios/withMobileVlcKit.js:
--------------------------------------------------------------------------------
1 | const { withDangerousMod } = require("@expo/config-plugins");
2 | const generateCode = require("@expo/config-plugins/build/utils/generateCode");
3 | const path = require("path");
4 | const fs = require("fs");
5 |
6 | const withMobileVlcKit = (config, options) => {
7 | // No need if you are running RN 0.61 and up
8 | if (!options?.ios?.includeVLCKit) {
9 | return config;
10 | }
11 |
12 | return withDangerousMod(config, [
13 | "ios",
14 | (config) => {
15 | const filePath = path.join(config.modRequest.platformProjectRoot, "Podfile");
16 |
17 | const contents = fs.readFileSync(filePath, "utf-8");
18 |
19 | const newCode = generateCode.mergeContents({
20 | tag: "withVlcMediaPlayer",
21 | src: contents,
22 | newSrc: " pod 'MobileVLCKit', '3.3.10'",
23 | anchor: /use\_expo\_modules\!/i,
24 | offset: 3,
25 | comment: " #",
26 | });
27 |
28 | fs.writeFileSync(filePath, newCode.contents);
29 |
30 | return config;
31 | },
32 | ]);
33 | };
34 |
35 | module.exports = withMobileVlcKit;
36 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | android {
3 | namespace 'com.yuanzhou.vlc'
4 | compileSdkVersion 35
5 |
6 | defaultConfig {
7 | minSdkVersion 26
8 | targetSdkVersion 35
9 | versionCode 1
10 | versionName "1.0"
11 | consumerProguardFiles("proguard-rules.pro")
12 | ndk {
13 | abiFilters 'armeabi-v7a','x86_64','arm64-v8a','x86'
14 | }
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | }
23 | buildscript {
24 | repositories {
25 | google()
26 | mavenCentral()
27 | // maven { url "https://maven.appspector.com/artifactory/android-sdk" }
28 |
29 | }
30 | dependencies {
31 | classpath("com.android.tools.build:gradle:4.0.2")
32 | }
33 | }
34 |
35 | repositories {
36 | maven {
37 | url "https://mvnrepository.com/artifact/org.videolan.android"
38 | }
39 | google()
40 | mavenCentral()
41 | }
42 |
43 | dependencies {
44 | implementation fileTree(dir: 'libs', include: ['*.jar'])
45 | implementation "com.facebook.react:react-native:+"
46 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
47 | implementation 'org.videolan.android:libvlc-all:3.6.3'
48 | }
49 |
--------------------------------------------------------------------------------
/playerView/BackHandle.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by aolc on 2018/5/22.
3 | */
4 |
5 | let backFunctionKeys = [];
6 | let backFunctionsMap = new Map();
7 |
8 | function removeIndex(array, index) {
9 | let newArray = [];
10 | for (let i = 0; i < array.length; i++) {
11 | if (i !== index) {
12 | newArray.push(array[i]);
13 | }
14 | }
15 | return newArray;
16 | }
17 |
18 | function removeKey(array, key) {
19 | let newArray = [];
20 | for (let i = 0; i < array.length; i++) {
21 | if (array[i] !== key) {
22 | newArray.push(array[i]);
23 | }
24 | }
25 | return newArray;
26 | }
27 |
28 | const handleBack = () => {
29 | if (backFunctionKeys.length > 0) {
30 | let functionKey = backFunctionKeys[backFunctionKeys.length - 1];
31 | backFunctionKeys = removeIndex(backFunctionKeys, backFunctionKeys.length - 1);
32 | let functionA = backFunctionsMap.get(functionKey);
33 | backFunctionsMap.delete(functionKey);
34 | functionA && functionA();
35 | return false;
36 | }
37 | return true;
38 | };
39 |
40 | const addBackFunction = (key, functionA) => {
41 | backFunctionsMap.set(key, functionA);
42 | backFunctionKeys.push(key);
43 | };
44 |
45 | const removeBackFunction = key => {
46 | backFunctionKeys = removeKey(backFunctionKeys, key);
47 | backFunctionsMap.delete(key);
48 | };
49 |
50 | export default {
51 | handleBack,
52 | addBackFunction,
53 | removeBackFunction,
54 | };
55 |
--------------------------------------------------------------------------------
/.github/workflows/npmpublish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | # Trigger the workflow on push or pull request,
8 | # but only for the master branch
9 | push:
10 | branches:
11 | - master
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 | - name: Set Git environment and update package number
21 | run: |
22 | git config --global user.name 'GIT Package Updater'
23 | git config --global user.email 'razorRun@users.noreply.github.com'
24 | npm version patch
25 | git push
26 | - run: npm ci
27 |
28 | publish-npm:
29 | needs: build
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: actions/setup-node@v4
34 | with:
35 | node-version: 20
36 | registry-url: https://registry.npmjs.org/
37 | - name: Pull latest Changes and publish new version
38 | run: git pull
39 | - run: npm ci
40 | - run: npm publish
41 | env:
42 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
43 |
--------------------------------------------------------------------------------
/.github/workflows/npmpublish-beta.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package Beta
5 |
6 | on:
7 | # Trigger the workflow from any feature branch
8 | workflow_dispatch:
9 | inputs:
10 | betaVersion:
11 | description: "Version tag for the beta job"
12 | required: true
13 | type: number
14 | jobs:
15 | publish-npm-beta:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: 20
22 | registry-url: https://registry.npmjs.org/
23 | - name: Set Git environment and update package number
24 | run: |
25 | sudo apt upgrade && sudo apt install jq -y
26 | npm --no-git-tag-version version patch
27 | currentVersion=$(jq --raw-output '.version' package.json)
28 | git config --global user.name 'GIT Package Updater'
29 | git config --global user.email 'razorRun@users.noreply.github.com'
30 | npm -f version "$currentVersion-beta.${{ github.event.inputs.betaVersion }}"
31 | git push
32 | - run: npm ci
33 | - run: npm publish --tag beta
34 | env:
35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
36 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer/RCTVLCPlayer.h:
--------------------------------------------------------------------------------
1 | #import "React/RCTView.h"
2 | #if TARGET_OS_TV
3 | #import
4 | #else
5 | #import
6 | #endif
7 |
8 | @class RCTEventDispatcher;
9 |
10 | @interface RCTVLCPlayer : UIView
11 |
12 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoProgress;
13 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoPaused;
14 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoStopped;
15 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoBuffering;
16 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoPlaying;
17 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoEnded;
18 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoError;
19 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoOpen;
20 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart;
21 | @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoad;
22 | @property (nonatomic, copy) RCTBubblingEventBlock onRecordingState;
23 | @property (nonatomic, copy) RCTBubblingEventBlock onSnapshot;
24 |
25 | @property (nonatomic, strong) VLCDialogProvider *dialogProvider;
26 | @property (nonatomic, assign) BOOL acceptInvalidCertificates;
27 |
28 | - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
29 | - (void)setMuted:(BOOL)value;
30 | - (void)startRecording:(NSString*)path;
31 | - (void)stopRecording;
32 | - (void)stopPlayer;
33 | - (void)snapshot:(NSString*)path;
34 | @end
35 |
--------------------------------------------------------------------------------
/playerView/TimeLimit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by yuanzhou.xu on 2018/5/16.
3 | */
4 | import React, { Component } from 'react';
5 | import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
6 |
7 | export default class TimeLimt extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.timer = null;
11 | }
12 |
13 | static defaultProps = {
14 | maxTime: 0,
15 | };
16 |
17 | state = {
18 | timeNumber: 0,
19 | };
20 |
21 | componentDidMount() {
22 | if(this.props.maxTime > 0){
23 | this.timer = setInterval(this._updateTimer, 1000);
24 | }
25 | }
26 |
27 | _updateTimer = () => {
28 | let { timeNumber } = this.state;
29 | let { maxTime } = this.props;
30 | let newTimeNumber = timeNumber + 1;
31 | this.setState({
32 | timeNumber: newTimeNumber,
33 | });
34 | if (newTimeNumber >= maxTime) {
35 | this._onEnd();
36 | }
37 | };
38 |
39 | componentWillUnmount() {
40 | clearInterval(this.timer);
41 | }
42 |
43 | _onEnd = () => {
44 | let { onEnd } = this.props;
45 | clearInterval(this.timer);
46 | onEnd && onEnd();
47 | };
48 |
49 | render() {
50 | let { timeNumber } = this.state;
51 | let { maxTime } = this.props;
52 | return (
53 |
57 | {maxTime > 0 && (
58 |
59 | {maxTime - timeNumber}
60 |
61 | )}
62 | //
63 | // 跳过片头
64 | //
65 |
66 | );
67 | }
68 | }
69 |
70 | const styles = StyleSheet.create({
71 | container: {
72 | flex: 1,
73 | //backgroundColor: '#000',
74 | },
75 | timeView: {
76 | height: '100%',
77 | justifyContent: 'center',
78 | alignItems: 'center',
79 | marginRight: 5,
80 | },
81 | nameView: {
82 | height: '100%',
83 | justifyContent: 'center',
84 | alignItems: 'center',
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/expo/android/withGradleTasks.js:
--------------------------------------------------------------------------------
1 | const { withAppBuildGradle } = require("@expo/config-plugins");
2 | const generateCode = require("@expo/config-plugins/build/utils/generateCode");
3 |
4 | const resolveAppGradleString = (options) => {
5 | // for React Native 0.71, the file value now contains "jetified-react-android" instead of "jetified-react-native"
6 | const rnJetifierName = options?.android?.legacyJetifier ? "jetified-react-native" : "jetified-react-android";
7 |
8 | const gradleString = `tasks.whenTaskAdded((tas -> {
9 | // when task is 'mergeLocalDebugNativeLibs' or 'mergeLocalReleaseNativeLibs'
10 | if (tas.name.contains("merge") && tas.name.contains("NativeLibs")) {
11 | tasks.named(tas.name) {it
12 | doFirst {
13 | java.nio.file.Path notNeededDirectory = it.externalLibNativeLibs
14 | .getFiles()
15 | .stream()
16 | .filter(file -> file.toString().contains("${rnJetifierName}"))
17 | .findAny()
18 | .orElse(null)
19 | .toPath();
20 | java.nio.file.Files.walk(notNeededDirectory).forEach(file -> {
21 | if (file.toString().contains("libc++_shared.so")) {
22 | java.nio.file.Files.delete(file);
23 | }
24 | });
25 | }
26 | }
27 | }
28 | }))`;
29 |
30 | return gradleString;
31 | };
32 |
33 | const withGradleTasks = (config, options) => {
34 | if(!options || !options.android){
35 | return config;
36 | }
37 | return withAppBuildGradle(config, (config) => {
38 | const newCode = generateCode.mergeContents({
39 | tag: "withVlcMediaPlayer",
40 | src: config.modResults.contents,
41 | newSrc: resolveAppGradleString(options),
42 | anchor: /applyNativeModulesAppBuildGradle\(project\)/i,
43 | offset: 2,
44 | comment: "//",
45 | });
46 |
47 | config.modResults.contents = newCode.contents;
48 |
49 | return config;
50 | });
51 | };
52 |
53 | module.exports = withGradleTasks;
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and not Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
107 | # Stores VSCode versions used for testing VSCode extensions
108 | .vscode-test
109 |
110 | # yarn v2
111 |
112 | .yarn/cache
113 | .yarn/unplugged
114 | .yarn/build-state.yml
115 | .pnp.*
116 |
117 | # Android
118 | .gradle
119 | .idea
120 | build/
121 | local.properties
122 |
123 | # MacOS
124 | .DS_Store
125 |
126 |
--------------------------------------------------------------------------------
/playerView/SizeController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 高度定义
3 | * Created by yuanzhou.xu on 17/2/18.
4 | */
5 | import { PixelRatio, Dimensions, Platform, StatusBar } from 'react-native';
6 | let initialDeviceHeight = 667;
7 | let initialDeviceWidth = 375;
8 | let initialPixelRatio = 2;
9 | let deviceHeight = Dimensions.get('window').height;
10 | let deviceWidth = Dimensions.get('window').width;
11 | let pixelRatio = PixelRatio.get();
12 | let statusBarHeight = 20; //初始状态栏高度
13 | let topBarHeight = 44; //初始导航栏高度
14 | let tabBarHeight = 49; //初始标签栏高度
15 | let IS_IPHONEX = false;
16 | let changeRatio = Math.min(
17 | deviceHeight / initialDeviceHeight,
18 | deviceWidth / initialDeviceWidth,
19 | ); //pixelRatio/initialPixelRatio;
20 | changeRatio = changeRatio.toFixed(2);
21 | if (deviceWidth > 375 && deviceWidth <= 1125 / 2) {
22 | statusBarHeight = 27;
23 | topBarHeight = 66;
24 | tabBarHeight = 60;
25 | } else if (deviceWidth > 1125 / 2) {
26 | statusBarHeight = 30;
27 | topBarHeight = 66;
28 | tabBarHeight = 60;
29 | }
30 | if (Platform.OS !== 'ios') {
31 | statusBarHeight = 20;
32 | if (deviceWidth > 375 && deviceWidth <= 1125 / 2) {
33 | statusBarHeight = 25;
34 | } else if (deviceWidth > 1125 / 2 && deviceWidth < 812) {
35 | statusBarHeight = 25;
36 | }
37 | if (StatusBar.currentHeight) {
38 | statusBarHeight = StatusBar.currentHeight;
39 | }
40 | }
41 |
42 | if (deviceWidth >= 375 && deviceWidth < 768) {
43 | topBarHeight = 44; //初始导航栏高度
44 | tabBarHeight = 49;
45 | changeRatio = 1;
46 | }
47 | if (deviceHeight >= 812) {
48 | statusBarHeight = 44;
49 | //topBarHeight = 60;
50 | IS_IPHONEX = true;
51 | }
52 | /**
53 | * 返回状态栏高度
54 | */
55 | export function getStatusBarHeight() {
56 | return statusBarHeight;
57 | }
58 | /**
59 | * 返回导航栏高度
60 | */
61 | export function getTopBarHeight() {
62 | return topBarHeight;
63 | }
64 | /**
65 | * 返回标签栏高度
66 | */
67 | export function getTabBarHeight() {
68 | return tabBarHeight;
69 | }
70 | /**
71 | *
72 | */
73 | export function getTopHeight() {
74 | if (Platform.OS === 'ios') {
75 | return topBarHeight + statusBarHeight;
76 | } else {
77 | return topBarHeight + statusBarHeight;
78 | }
79 | }
80 | /**
81 | * 返回变更比例
82 | */
83 | export function getChangeRatio() {
84 | return changeRatio;
85 | }
86 | /** 获取tabBar比例**/
87 | export function getTabBarRatio() {
88 | return tabBarHeight / 49;
89 | }
90 |
91 | /**
92 | * 获取TopBar比例
93 | */
94 | export function getTopBarRatio() {
95 | return changeRatio;
96 | }
97 |
98 | export function isIphoneX() {
99 | return IS_IPHONEX;
100 | }
101 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer/RCTVLCPlayerManager.m:
--------------------------------------------------------------------------------
1 | #import "RCTVLCPlayerManager.h"
2 | #import "RCTVLCPlayer.h"
3 | #import "React/RCTBridge.h"
4 | #import "React/RCTUIManager.h"
5 |
6 | @implementation RCTVLCPlayerManager
7 |
8 | RCT_EXPORT_MODULE();
9 |
10 | @synthesize bridge = _bridge;
11 |
12 | - (UIView *)view
13 | {
14 | return [[RCTVLCPlayer alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
15 | }
16 |
17 | /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
18 | RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock);
19 | RCT_EXPORT_VIEW_PROPERTY(onVideoPaused, RCTDirectEventBlock);
20 | RCT_EXPORT_VIEW_PROPERTY(onVideoStopped, RCTDirectEventBlock);
21 | RCT_EXPORT_VIEW_PROPERTY(onVideoBuffering, RCTDirectEventBlock);
22 | RCT_EXPORT_VIEW_PROPERTY(onVideoPlaying, RCTDirectEventBlock);
23 | RCT_EXPORT_VIEW_PROPERTY(onVideoEnded, RCTDirectEventBlock);
24 | RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock);
25 | RCT_EXPORT_VIEW_PROPERTY(onVideoOpen, RCTDirectEventBlock);
26 | RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
27 | RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock);
28 | RCT_EXPORT_VIEW_PROPERTY(onRecordingState, RCTDirectEventBlock);
29 | RCT_EXPORT_VIEW_PROPERTY(onSnapshot, RCTDirectEventBlock);
30 |
31 | - (dispatch_queue_t)methodQueue
32 | {
33 | return dispatch_get_main_queue();
34 | }
35 |
36 | RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary);
37 | RCT_EXPORT_VIEW_PROPERTY(subtitleUri, NSString);
38 | RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
39 | RCT_EXPORT_VIEW_PROPERTY(seek, float);
40 | RCT_EXPORT_VIEW_PROPERTY(rate, float);
41 | RCT_EXPORT_VIEW_PROPERTY(resume, BOOL);
42 | RCT_EXPORT_VIEW_PROPERTY(videoAspectRatio, NSString);
43 | RCT_EXPORT_VIEW_PROPERTY(snapshotPath, NSString);
44 | RCT_CUSTOM_VIEW_PROPERTY(muted, BOOL, RCTVLCPlayer)
45 | {
46 | BOOL isMuted = [RCTConvert BOOL:json];
47 | [view setMuted:isMuted];
48 | };
49 | RCT_EXPORT_VIEW_PROPERTY(audioTrack, int);
50 | RCT_EXPORT_VIEW_PROPERTY(textTrack, int);
51 | RCT_EXPORT_VIEW_PROPERTY(autoplay, BOOL);
52 | RCT_EXPORT_VIEW_PROPERTY(acceptInvalidCertificates, BOOL);
53 |
54 | RCT_EXPORT_METHOD(startRecording:(nonnull NSNumber*) reactTag withPath:(NSString *)path) {
55 | [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
56 | RCTVLCPlayer *view = viewRegistry[reactTag];
57 | if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
58 | RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
59 | return;
60 | }
61 | [view startRecording:path];
62 | }];
63 | }
64 |
65 | RCT_EXPORT_METHOD(stopRecording:(nonnull NSNumber*) reactTag) {
66 | [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
67 | RCTVLCPlayer *view = viewRegistry[reactTag];
68 | if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
69 | RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
70 | return;
71 | }
72 | [view stopRecording];
73 | }];
74 | }
75 |
76 | RCT_EXPORT_METHOD(stopPlayer:(nonnull NSNumber*) reactTag) {
77 | [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
78 | RCTVLCPlayer *view = viewRegistry[reactTag];
79 | if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
80 | RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
81 | return;
82 | }
83 | [view stopPlayer];
84 | }];
85 | }
86 |
87 | RCT_EXPORT_METHOD(snapshot:(nonnull NSNumber*) reactTag withPath:(NSString *)path) {
88 | [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
89 | RCTVLCPlayer *view = viewRegistry[reactTag];
90 | if (!view || ![view isKindOfClass:[RCTVLCPlayer class]]) {
91 | RCTLogError(@"Cannot find RCTVLCPlayer with tag #%@", reactTag);
92 | return;
93 | }
94 | [view snapshot:path];
95 | }];
96 | }
97 |
98 | @end
99 |
--------------------------------------------------------------------------------
/android/src/main/java/com/yuanzhou/vlc/vlcplayer/VideoEventEmitter.java:
--------------------------------------------------------------------------------
1 | package com.yuanzhou.vlc.vlcplayer;
2 |
3 | import androidx.annotation.StringDef;
4 | import android.util.Log;
5 | import android.view.View;
6 |
7 | import com.facebook.react.bridge.Arguments;
8 | import com.facebook.react.bridge.ReactContext;
9 | import com.facebook.react.bridge.WritableArray;
10 | import com.facebook.react.bridge.WritableMap;
11 | import com.facebook.react.uimanager.events.RCTEventEmitter;
12 |
13 |
14 | import java.lang.annotation.Retention;
15 | import java.lang.annotation.RetentionPolicy;
16 |
17 | class VideoEventEmitter {
18 |
19 | private final RCTEventEmitter eventEmitter;
20 |
21 | private int viewId = View.NO_ID;
22 |
23 | VideoEventEmitter(ReactContext reactContext) {
24 | this.eventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
25 | }
26 |
27 | public static final String EVENT_LOAD_START = "onVideoLoadStart";
28 | public static final String EVENT_ON_OPEN = "onVideoOpen";
29 | public static final String EVENT_PROGRESS = "onVideoProgress";
30 | public static final String EVENT_SEEK = "onVideoSeek";
31 | public static final String EVENT_END = "onVideoEnd";
32 | public static final String EVENT_ON_IS_PLAYING= "onVideoPlaying";
33 | public static final String EVENT_ON_VIDEO_STATE_CHANGE = "onVideoStateChange";
34 | public static final String EVENT_ON_VIDEO_STOPPED = "onVideoStopped";
35 | public static final String EVENT_ON_ERROR = "onVideoError";
36 | public static final String EVENT_ON_VIDEO_BUFFERING = "onVideoBuffering";
37 | public static final String EVENT_ON_PAUSED = "onVideoPaused";
38 | public static final String EVENT_ON_LOAD = "onVideoLoad";
39 | public static final String EVENT_RECORDING_STATE = "onRecordingState";
40 | public static final String EVENT_ON_SNAPSHOT = "onSnapshot";
41 |
42 | static final String[] Events = {
43 | EVENT_LOAD_START,
44 | EVENT_PROGRESS,
45 | EVENT_SEEK,
46 | EVENT_END,
47 | EVENT_ON_IS_PLAYING,
48 | EVENT_ON_VIDEO_STATE_CHANGE,
49 | EVENT_ON_OPEN,
50 | EVENT_ON_PAUSED,
51 | EVENT_ON_VIDEO_BUFFERING,
52 | EVENT_ON_ERROR,
53 | EVENT_ON_VIDEO_STOPPED,
54 | EVENT_ON_LOAD,
55 | EVENT_RECORDING_STATE,
56 | EVENT_ON_SNAPSHOT
57 | };
58 |
59 | @Retention(RetentionPolicy.SOURCE)
60 | @StringDef({
61 | EVENT_LOAD_START,
62 | EVENT_PROGRESS,
63 | EVENT_SEEK,
64 | EVENT_END,
65 | EVENT_ON_IS_PLAYING,
66 | EVENT_ON_VIDEO_STATE_CHANGE,
67 | EVENT_ON_OPEN,
68 | EVENT_ON_PAUSED,
69 | EVENT_ON_VIDEO_BUFFERING,
70 | EVENT_ON_ERROR,
71 | EVENT_ON_VIDEO_STOPPED,
72 | EVENT_ON_LOAD,
73 | EVENT_RECORDING_STATE,
74 | EVENT_ON_SNAPSHOT
75 | })
76 |
77 | @interface VideoEvents {
78 | }
79 |
80 | private static final String EVENT_PROP_ERROR = "error";
81 | private static final String EVENT_PROP_ERROR_STRING = "errorString";
82 | private static final String EVENT_PROP_ERROR_EXCEPTION = "";
83 |
84 |
85 | void setViewId(int viewId) {
86 | this.viewId = viewId;
87 | }
88 |
89 | /**
90 | * MideaPlayer初始化完毕回调
91 | */
92 | void loadStart() {
93 | WritableMap event = Arguments.createMap();
94 | receiveEvent(EVENT_LOAD_START, event);
95 | }
96 |
97 |
98 | /**
99 | * 视频进度改变回调
100 | * @param currentPosition
101 | * @param bufferedDuration
102 | */
103 | void progressChanged(double currentPosition, double bufferedDuration) {
104 | WritableMap event = Arguments.createMap();
105 | event.putDouble("currentTime", currentPosition);
106 | event.putDouble("duration", bufferedDuration);
107 | receiveEvent(EVENT_PROGRESS, event);
108 | }
109 |
110 |
111 | void error(String errorString, Exception exception) {
112 | WritableMap error = Arguments.createMap();
113 | error.putString(EVENT_PROP_ERROR_STRING, errorString);
114 | error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.getMessage());
115 | WritableMap event = Arguments.createMap();
116 | event.putMap(EVENT_PROP_ERROR, error);
117 | //receiveEvent(EVENT_ERROR, event);
118 | }
119 |
120 | /**
121 | * 是否播放回调
122 | * @param isPlaying
123 | */
124 | void isPlaying(boolean isPlaying){
125 | WritableMap map = Arguments.createMap();
126 | map.putBoolean("isPlaying",isPlaying);
127 | receiveEvent(EVENT_ON_IS_PLAYING, map);
128 | }
129 |
130 | /**
131 | * 视频状态改变回调
132 | * @param map
133 | */
134 | void onVideoStateChange(WritableMap map){
135 | receiveEvent(EVENT_ON_VIDEO_STATE_CHANGE, map);
136 | }
137 |
138 | void sendEvent(WritableMap map, String event) {
139 | receiveEvent(event, map);
140 | }
141 |
142 | private void receiveEvent(@VideoEvents String type, WritableMap event) {
143 | eventEmitter.receiveEvent(viewId, type, event);
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/playerView/ControlBtn.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by yuanzhou.xu on 2018/5/16.
3 | */
4 | import React, { Component } from 'react';
5 | import {
6 | StyleSheet,
7 | Text,
8 | View,
9 | Dimensions,
10 | TouchableOpacity,
11 | ActivityIndicator,
12 | StatusBar,
13 | } from 'react-native';
14 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
15 | // import Slider from 'react-native-slider';
16 | import PropTypes from 'prop-types';
17 | import TimeLimt from './TimeLimit';
18 |
19 | export default class ControlBtn extends Component {
20 | static defaultProps = {
21 | titleGolive: 'Go live',
22 | showLeftButton: true,
23 | showMiddleButton: true,
24 | showRightButton: true
25 | }
26 |
27 | _getTime = (data = 0) => {
28 | let hourCourse = Math.floor(data / 3600);
29 | let diffCourse = data % 3600;
30 | let minCourse = Math.floor(diffCourse / 60);
31 | let secondCourse = Math.floor(diffCourse % 60);
32 | let courseReal = '';
33 | if (hourCourse) {
34 | if (hourCourse < 10) {
35 | courseReal += '0' + hourCourse + ':';
36 | } else {
37 | courseReal += hourCourse + ':';
38 | }
39 | }
40 | if (minCourse < 10) {
41 | courseReal += '0' + minCourse + ':';
42 | } else {
43 | courseReal += minCourse + ':';
44 | }
45 | if (secondCourse < 10) {
46 | courseReal += '0' + secondCourse;
47 | } else {
48 | courseReal += secondCourse;
49 | }
50 | return courseReal;
51 | };
52 |
53 | render() {
54 | let {
55 | paused,
56 | isFull,
57 | showGG,
58 | showSlider,
59 | showGoLive,
60 | onGoLivePress,
61 | onReplayPress,
62 | onPausedPress,
63 | onFullPress,
64 | onValueChange,
65 | onSlidingComplete,
66 | currentTime,
67 | totalTime,
68 | onLeftPress,
69 | title,
70 | onEnd,
71 | titleGolive,
72 | showLeftButton,
73 | showMiddleButton,
74 | showRightButton,
75 | style
76 | } = this.props;
77 | return (
78 |
79 |
80 |
81 |
82 |
83 | {
84 | showLeftButton ? (
85 | {
88 | onReplayPress && onReplayPress();
89 | }}
90 | style={{ width: 50, alignItems: 'center', justifyContent: 'center' }}>
91 |
92 |
93 | ) :
94 | }
95 |
97 |
98 |
99 | {
100 | showMiddleButton && (
101 | {
104 | onPausedPress && onPausedPress(!paused);
105 | }}
106 | style={{ width: 50, alignItems: 'center', justifyContent: 'center' }}>
107 |
108 |
109 | )
110 | }
111 |
112 | {/* {showSlider && totalTime > 0 &&(
113 |
120 |
121 |
122 | {this._getTime(currentTime) || 0}
123 |
124 |
125 |
126 | {
134 | onValueChange && onValueChange(value);
135 | }}
136 | onSlidingComplete={value => {
137 | onSlidingComplete && onSlidingComplete(value);
138 | }}
139 | />
140 |
141 |
142 |
144 | {this._getTime(totalTime) || 0}
145 |
146 |
147 |
148 | )} */}
149 |
150 |
151 | {
154 | onGoLivePress && onGoLivePress();
155 | }}>
156 | {showGoLive ? titleGolive : ' '}
158 |
159 | {
160 | showRightButton ? (
161 | {
164 | onFullPress && onFullPress(!isFull);
165 | }}
166 | style={{ width: 50, alignItems: 'center', justifyContent: 'center' }}>
167 |
168 |
169 | ) :
170 | }
171 |
172 |
173 |
174 |
175 |
176 |
177 | );
178 | }
179 | }
180 |
181 | const styles = StyleSheet.create({
182 | container: {
183 | flex: 1,
184 | //backgroundColor: '#000',
185 | },
186 | controls: {
187 | width: '100%',
188 | height: 50,
189 | },
190 | rateControl: {
191 | flex: 0,
192 | flexDirection: 'row',
193 | marginTop: 10,
194 | marginLeft: 10,
195 | //backgroundColor: 'rgba(0,0,0,0.5)',
196 | width: 120,
197 | height: 30,
198 | justifyContent: 'space-around',
199 | alignItems: 'center',
200 | borderRadius: 10,
201 | },
202 | controlOption: {
203 | textAlign: 'center',
204 | fontSize: 13,
205 | color: '#fff',
206 | width: 30,
207 | //lineHeight: 12,
208 | },
209 | controlContainer: {
210 | flex: 1,
211 | //padding: 5,
212 | alignItems: 'center',
213 | justifyContent: 'center',
214 | },
215 |
216 | controlContent: {
217 | width: '100%',
218 | height: 50,
219 | //borderRadius: 10,
220 | backgroundColor: 'rgba(255,255,255,0.1)',
221 | },
222 | controlContent2: {
223 | flex: 1,
224 | flexDirection: 'row',
225 | backgroundColor: 'rgba(0,0,0,0.5)',
226 | alignItems: 'center',
227 | justifyContent: 'space-between',
228 | },
229 |
230 | progress: {
231 | flex: 1,
232 | borderRadius: 3,
233 | alignItems: 'center',
234 | justifyContent: 'center',
235 | },
236 | left: {
237 | flexDirection: 'row',
238 | alignItems: 'center',
239 | justifyContent: 'center',
240 | },
241 | right: {
242 | flexDirection: 'row',
243 | alignItems: 'center',
244 | justifyContent: 'center',
245 | },
246 | thumb: {
247 | width: 6,
248 | height: 18,
249 | backgroundColor: '#fff',
250 | borderRadius: 4,
251 | },
252 | loading: {
253 | position: 'absolute',
254 | left: 0,
255 | top: 0,
256 | zIndex: 0,
257 | width: '100%',
258 | height: '100%',
259 | justifyContent: 'center',
260 | alignItems: 'center',
261 | },
262 |
263 | GG: {
264 | backgroundColor: 'rgba(255,255,255,1)',
265 | height: 30,
266 | paddingLeft: 10,
267 | paddingRight: 10,
268 | borderRadius: 20,
269 | justifyContent: 'center',
270 | alignItems: 'center',
271 | },
272 | });
273 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { Component } from "react";
2 | import { StyleProp, ViewStyle } from "react-native";
3 |
4 | /**
5 | * Video aspect ratio type
6 | */
7 | export type PlayerAspectRatio = "16:9" | "1:1" | "4:3" | "3:2" | "21:9" | "9:16";
8 |
9 | /**
10 | * Video resize mode
11 | */
12 | export type PlayerResizeMode = "fill" | "contain" | "cover" | "none" | "scale-down";
13 |
14 | /**
15 | * VLC Player source configuration options
16 | */
17 | export interface VLCPlayerSource {
18 | /**
19 | * Media source URI to render
20 | */
21 | uri: string;
22 | /**
23 | * VLC Player initialization type
24 | *
25 | * - Default configuration: `1`
26 | * - Custom configuration: `2`
27 | *
28 | * See `initOptions` for more information
29 | *
30 | * @default 1
31 | */
32 | initType?: 1 | 2;
33 | /**
34 | * https://wiki.videolan.org/VLC_command-line_help/
35 | *
36 | * VLC Player initialization options
37 | *
38 | * `["--network-caching=50", "--rtsp-tcp"]`
39 | *
40 | * If `repeat` is set on props this will default to ["--repeat"] unless
41 | * another `--repeat` or `--input-repeat` flag is passed.
42 | *
43 | * @default []
44 | */
45 | initOptions?: string[];
46 | }
47 |
48 | /**
49 | * Represents a track type in playback
50 | */
51 | export type Track = {
52 | /**
53 | * Track identification
54 | */
55 | id: number;
56 |
57 | /**
58 | * Track name
59 | */
60 | name: string;
61 | };
62 |
63 | /**
64 | * Represents a full playback information
65 | */
66 | export type VideoInfo = {
67 | /**
68 | * Total playback duration
69 | */
70 | duration: number;
71 |
72 | /**
73 | * Playback target
74 | */
75 | target: number;
76 |
77 | /**
78 | * Total playback video size
79 | */
80 | videoSize: Record<"width" | "height", number>;
81 |
82 | /**
83 | * List of playback audio tracks
84 | */
85 | audioTracks: Track[];
86 |
87 | /**
88 | * List of playback text tracks
89 | */
90 | textTracks: Track[];
91 | };
92 |
93 | type OnPlayingEventProps = Pick & {
94 | seekable: boolean;
95 | };
96 |
97 | type OnProgressEventProps = Pick & {
98 | /**
99 | * Current playback time
100 | */
101 | currentTime: number;
102 |
103 | /**
104 | * Current playback position
105 | */
106 | position: number;
107 |
108 | /**
109 | * Remaining time to end playback
110 | */
111 | remainingTime: number;
112 | };
113 |
114 | type SimpleCallbackEventProps = Pick;
115 |
116 | export type VLCPlayerCallbackProps = {
117 | /**
118 | * Called when media starts playing returns
119 | *
120 | * @param event - Event properties
121 | */
122 | onPlaying?: (event: OnPlayingEventProps) => void;
123 |
124 | /**
125 | * Callback containing position as a fraction, and duration, currentTime and remainingTime in seconds
126 | *
127 | * @param event - Event properties
128 | */
129 | onProgress?: (event: OnProgressEventProps) => void;
130 |
131 | /**
132 | * Called when media is paused
133 | *
134 | * @param event - Event properties
135 | */
136 | onPaused?: (event: SimpleCallbackEventProps) => void;
137 |
138 | /**
139 | * Called when media is stoped
140 | *
141 | * @param event - Event properties
142 | */
143 | onStopped?: (event: SimpleCallbackEventProps) => void;
144 |
145 | /**
146 | * Called when media is buffering
147 | *
148 | * @param event - Event properties
149 | */
150 | onBuffering?: (event: SimpleCallbackEventProps) => void;
151 |
152 | /**
153 | * Called when media playing ends
154 | *
155 | * @param event - Event properties
156 | */
157 | onEnd?: (event: SimpleCallbackEventProps) => void;
158 |
159 | /**
160 | * Called when an error occurs whilst attempting to play media
161 | *
162 | * @param event - Event properties
163 | */
164 | onError?: (event: SimpleCallbackEventProps) => void;
165 |
166 | /**
167 | * Called when video info is loaded, Callback containing `VideoInfo`
168 | *
169 | * @param event - Event properties
170 | */
171 | onLoad?: (event: VideoInfo) => void;
172 |
173 | /**
174 | * Called when a new recording is created
175 | *
176 | * @param recordingPath - Full path to the recording file
177 | */
178 | onRecordingCreated?: (recordingPath: string) => void;
179 |
180 | /**
181 | * Called when a new snapshot is created
182 | *
183 | * @param event - Event properties
184 | */
185 | onSnapshot?: (event: {
186 | success: boolean;
187 | path?: string;
188 | error?: string;
189 | }) => void;
190 | };
191 |
192 | export type VLCPlayerProps = VLCPlayerCallbackProps & {
193 | /**
194 | * Object that contains the uri of a video or song to play eg
195 | */
196 | source: VLCPlayerSource;
197 |
198 | /**
199 | * local subtitle file path,if you want to hide subtitle,
200 | * you can set this to an empty subtitle file,
201 | * current we don't support a hide subtitle prop.
202 | */
203 | subtitleUri?: string;
204 |
205 | /**
206 | * Set to `true` or `false` to pause or play the media
207 | * @default false
208 | */
209 | paused?: boolean;
210 |
211 | /**
212 | * Set to `true` or `false` to loop the media
213 | * @default false
214 | */
215 | repeat?: boolean;
216 |
217 | /**
218 | * Set the playback rate of the player
219 | * @default 1
220 | */
221 | rate?: number;
222 |
223 | /**
224 | * Set position to seek between 0 and 1
225 | * (0 being the start, 1 being the end, use position from the progress object)
226 | */
227 | seek?: number;
228 |
229 | /**
230 | * Set the volume of the player
231 | */
232 | volume?: number;
233 |
234 | /**
235 | * Set to `true` or `false` to mute the player
236 | * @default false
237 | */
238 | muted?: boolean;
239 |
240 | /**
241 | * Set audioTrack id (number) (see onLoad callback VideoInfo.audioTracks)
242 | */
243 | audioTrack?: number;
244 |
245 | /**
246 | * Set textTrack(subtitle) id (number) (see onLoad callback - VideoInfo.textTracks)
247 | */
248 | textTrack?: number;
249 |
250 | /**
251 | * Set to `true` or `false` to allow playing in the background
252 | * @default false
253 | */
254 | playInBackground?: boolean;
255 |
256 | /**
257 | * Video aspect ratio
258 | */
259 | videoAspectRatio?: PlayerAspectRatio;
260 |
261 | /**
262 | * Set to `true` or `false` to enable auto aspect ratio
263 | * @default false
264 | */
265 | autoAspectRatio?: boolean;
266 |
267 | /**
268 | * Set the behavior for the video size (fill, contain, cover, none, scale-down)
269 | */
270 | resizeMode?: PlayerResizeMode;
271 |
272 | /**
273 | * React native view stylesheet styles
274 | */
275 | style?: StyleProp;
276 |
277 | /**
278 | * Enables autoplay
279 | *
280 | * @default true
281 | */
282 | autoplay?: boolean;
283 |
284 | /**
285 | * Set to `true` to automatically accept invalid SSL/TLS certificates
286 | * when connecting to HTTPS streams. This bypasses certificate validation
287 | * which may pose security risks.
288 | *
289 | * @default false
290 | */
291 | acceptInvalidCertificates?: boolean;
292 | };
293 |
294 | declare class PlaybackMethods extends Component {
295 | /**
296 | * Start a new recording session at the given path
297 | * @param path Directory to create new recording in
298 | */
299 | startRecording(path: string);
300 |
301 | /**
302 | * Stop current recording session
303 | */
304 | stopRecording();
305 |
306 | /**
307 | * Stop playing
308 | */
309 | stopPlayer();
310 |
311 | /**
312 | * Take a screenshot of the current video frame
313 | *
314 | * @param path The file path where to save the screenshot
315 | */
316 | snapshot(path: string);
317 |
318 | /**
319 | * Seek to the given position
320 | *
321 | * @param pos Position to seek to (as a percentage of the full duration)
322 | */
323 | seek(pos: number);
324 |
325 | /**
326 | * Resume playback
327 | */
328 | resume();
329 |
330 | /**
331 | * Change auto aspect ratio setting
332 | *
333 | * @param useAuto Whether or not to use auto aspect ratio
334 | */
335 | autoAspectRatio(useAuto: boolean);
336 |
337 | /**
338 | * Update video aspect ratio e.g. `"16:9"`
339 | *
340 | * @param ratio Aspect ratio to use
341 | */
342 | changeVideoAspectRatio(ratio: string);
343 | }
344 |
345 | /**
346 | * A component that can be used to show a playback
347 | */
348 | declare class VLCPlayer extends PlaybackMethods {}
349 |
350 | /**
351 | * A component that renders a playback with additional
352 | * features like fullscreen, controls, etc.
353 | */
354 | declare class VlCPlayerView extends PlaybackMethods {}
355 |
--------------------------------------------------------------------------------
/VLCPlayer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactNative from "react-native";
3 |
4 | const { Component } = React;
5 |
6 | import PropTypes from "prop-types";
7 | import resolveAssetSource from "react-native/Libraries/Image/resolveAssetSource";
8 |
9 | const { StyleSheet, requireNativeComponent, NativeModules, View, UIManager } = ReactNative;
10 |
11 | export default class VLCPlayer extends Component {
12 | constructor(props, context) {
13 | super(props, context);
14 | this.seek = this.seek.bind(this);
15 | this.resume = this.resume.bind(this);
16 | this._assignRoot = this._assignRoot.bind(this);
17 | this._onError = this._onError.bind(this);
18 | this._onProgress = this._onProgress.bind(this);
19 | this._onEnded = this._onEnded.bind(this);
20 | this._onPlaying = this._onPlaying.bind(this);
21 | this._onStopped = this._onStopped.bind(this);
22 | this._onPaused = this._onPaused.bind(this);
23 | this._onBuffering = this._onBuffering.bind(this);
24 | this._onOpen = this._onOpen.bind(this);
25 | this._onLoadStart = this._onLoadStart.bind(this);
26 | this._onLoad = this._onLoad.bind(this);
27 | this._onRecordingState = this._onRecordingState.bind(this);
28 | this._onSnapshot = this._onSnapshot.bind(this);
29 | this.changeVideoAspectRatio = this.changeVideoAspectRatio.bind(this);
30 | }
31 | static defaultProps = {
32 | autoplay: true,
33 | };
34 |
35 | setNativeProps(nativeProps) {
36 | this._root.setNativeProps(nativeProps);
37 | }
38 |
39 | startRecording(path) {
40 | UIManager.dispatchViewManagerCommand(
41 | ReactNative.findNodeHandle(this),
42 | UIManager.getViewManagerConfig('RCTVLCPlayer').Commands
43 | .startRecording,
44 | [path],
45 | );
46 | }
47 |
48 | stopRecording() {
49 | UIManager.dispatchViewManagerCommand(
50 | ReactNative.findNodeHandle(this),
51 | UIManager.getViewManagerConfig('RCTVLCPlayer').Commands.stopRecording,
52 | []
53 | );
54 | }
55 |
56 | stopPlayer() {
57 | UIManager.dispatchViewManagerCommand(
58 | ReactNative.findNodeHandle(this),
59 | UIManager.getViewManagerConfig('RCTVLCPlayer').Commands.stopPlayer,
60 | []
61 | );
62 | }
63 |
64 | snapshot(path) {
65 | UIManager.dispatchViewManagerCommand(
66 | ReactNative.findNodeHandle(this),
67 | UIManager.getViewManagerConfig('RCTVLCPlayer').Commands.snapshot,
68 | [path]
69 | );
70 | }
71 |
72 | seek(pos) {
73 | this.setNativeProps({ seek: pos });
74 | }
75 |
76 | resume(isResume) {
77 | this.setNativeProps({ resume: isResume });
78 | }
79 |
80 | autoAspectRatio(isAuto) {
81 | this.setNativeProps({ autoAspectRatio: isAuto });
82 | }
83 |
84 | changeVideoAspectRatio(ratio) {
85 | this.setNativeProps({ videoAspectRatio: ratio });
86 | }
87 |
88 | _assignRoot(component) {
89 | this._root = component;
90 | }
91 |
92 | _onBuffering(event) {
93 | if (this.props.onBuffering) {
94 | this.props.onBuffering(event.nativeEvent);
95 | }
96 | }
97 |
98 | _onError(event) {
99 | if (this.props.onError) {
100 | this.props.onError(event.nativeEvent);
101 | }
102 | }
103 |
104 | _onOpen(event) {
105 | if (this.props.onOpen) {
106 | this.props.onOpen(event.nativeEvent);
107 | }
108 | }
109 |
110 | _onLoadStart(event) {
111 | if (this.props.onLoadStart) {
112 | this.props.onLoadStart(event.nativeEvent);
113 | }
114 | }
115 |
116 | _onProgress(event) {
117 | if (this.props.onProgress) {
118 | this.props.onProgress(event.nativeEvent);
119 | }
120 | }
121 |
122 | _onEnded(event) {
123 | if (this.props.onEnd) {
124 | this.props.onEnd(event.nativeEvent);
125 | }
126 | }
127 |
128 | _onStopped() {
129 | this.setNativeProps({ paused: true });
130 | if (this.props.onStopped) {
131 | this.props.onStopped();
132 | }
133 | }
134 |
135 | _onPaused(event) {
136 | if (this.props.onPaused) {
137 | this.props.onPaused(event.nativeEvent);
138 | }
139 | }
140 |
141 | _onPlaying(event) {
142 | if (this.props.onPlaying) {
143 | this.props.onPlaying(event.nativeEvent);
144 | }
145 | }
146 |
147 | _onLoad(event) {
148 | if (this.props.onLoad) {
149 | this.props.onLoad(event.nativeEvent);
150 | }
151 | }
152 |
153 | _onRecordingState(event) {
154 | if (this.lastRecording === event.nativeEvent.recordPath) {
155 | return;
156 | }
157 |
158 | if (!event.nativeEvent.isRecording && event.nativeEvent.recordPath) {
159 | this.lastRecording = event.nativeEvent.recordPath;
160 | this.props.onRecordingCreated(this.lastRecording);
161 | }
162 | }
163 |
164 | _onSnapshot(event) {
165 | if (event.nativeEvent.success && this.props.onSnapshot) {
166 | this.props.onSnapshot(event.nativeEvent);
167 | }
168 | }
169 |
170 | render() {
171 | /* const {
172 | source
173 | } = this.props;*/
174 | const source = resolveAssetSource(this.props.source) || {};
175 |
176 | let uri = source.uri || "";
177 | if (uri && uri.match(/^\//)) {
178 | uri = `file://${uri}`;
179 | }
180 |
181 | let isNetwork = !!(uri && uri.match(/^https?:/));
182 | const isAsset = !!(
183 | uri && uri.match(/^(assets-library|file|content|ms-appx|ms-appdata):/)
184 | );
185 | if (!isAsset) {
186 | isNetwork = true;
187 | }
188 | if (uri && uri.match(/^\//)) {
189 | isNetwork = false;
190 | }
191 | source.isNetwork = isNetwork;
192 | source.autoplay = this.props.autoplay;
193 | source.initOptions = source.initOptions || [];
194 |
195 | if (this.props.repeat) {
196 | const existingRepeat = source.initOptions.find(item => item.startsWith('--repeat') || item.startsWith('--input-repeat'));
197 | if (!existingRepeat) {
198 | source.initOptions.push("--repeat");
199 | }
200 | }
201 |
202 | const nativeProps = Object.assign({}, this.props);
203 | Object.assign(nativeProps, {
204 | style: [styles.base, nativeProps.style],
205 | source: source,
206 | src: {
207 | uri,
208 | isNetwork,
209 | isAsset,
210 | type: source.type || "",
211 | mainVer: source.mainVer || 0,
212 | patchVer: source.patchVer || 0,
213 | },
214 | onVideoLoadStart: this._onLoadStart,
215 | onVideoOpen: this._onOpen,
216 | onVideoError: this._onError,
217 | onVideoProgress: this._onProgress,
218 | onVideoEnded: this._onEnded,
219 | onVideoEnd: this._onEnded,
220 | onVideoPlaying: this._onPlaying,
221 | onVideoPaused: this._onPaused,
222 | onVideoStopped: this._onStopped,
223 | onVideoBuffering: this._onBuffering,
224 | onVideoLoad: this._onLoad,
225 | onRecordingState: this._onRecordingState,
226 | onSnapshot: this._onSnapshot,
227 | progressUpdateInterval: this.props.onProgress ? 250 : 0,
228 | });
229 |
230 | return ;
231 | }
232 | }
233 |
234 | VLCPlayer.propTypes = {
235 | /* Native only */
236 | rate: PropTypes.number,
237 | seek: PropTypes.number,
238 | resume: PropTypes.bool,
239 | paused: PropTypes.bool,
240 |
241 | autoAspectRatio: PropTypes.bool,
242 | videoAspectRatio: PropTypes.string,
243 | volume: PropTypes.number,
244 | disableFocus: PropTypes.bool,
245 | src: PropTypes.string,
246 | playInBackground: PropTypes.bool,
247 | playWhenInactive: PropTypes.bool,
248 | resizeMode: PropTypes.string,
249 | poster: PropTypes.string,
250 | repeat: PropTypes.bool,
251 | muted: PropTypes.bool,
252 | audioTrack: PropTypes.number,
253 | textTrack: PropTypes.number,
254 | acceptInvalidCertificates: PropTypes.bool,
255 |
256 | onVideoLoadStart: PropTypes.func,
257 | onVideoError: PropTypes.func,
258 | onVideoProgress: PropTypes.func,
259 | onVideoEnded: PropTypes.func,
260 | onVideoPlaying: PropTypes.func,
261 | onVideoPaused: PropTypes.func,
262 | onVideoStopped: PropTypes.func,
263 | onVideoBuffering: PropTypes.func,
264 | onVideoOpen: PropTypes.func,
265 | onVideoLoad: PropTypes.func,
266 |
267 | /* Wrapper component */
268 | source: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
269 | subtitleUri: PropTypes.string,
270 | autoplay: PropTypes.bool,
271 |
272 | onError: PropTypes.func,
273 | onProgress: PropTypes.func,
274 | onEnded: PropTypes.func,
275 | onStopped: PropTypes.func,
276 | onPlaying: PropTypes.func,
277 | onPaused: PropTypes.func,
278 | onRecordingCreated: PropTypes.func,
279 |
280 | /* Required by react-native */
281 | scaleX: PropTypes.number,
282 | scaleY: PropTypes.number,
283 | translateX: PropTypes.number,
284 | translateY: PropTypes.number,
285 | rotation: PropTypes.number,
286 | ...View.propTypes,
287 | };
288 |
289 | const styles = StyleSheet.create({
290 | base: {
291 | overflow: "hidden",
292 | },
293 | });
294 | const RCTVLCPlayer = requireNativeComponent("RCTVLCPlayer", VLCPlayer);
295 |
--------------------------------------------------------------------------------
/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerViewManager.java:
--------------------------------------------------------------------------------
1 | package com.yuanzhou.vlc.vlcplayer;
2 |
3 | import android.content.Context;
4 | import android.text.TextUtils;
5 |
6 | import com.facebook.react.bridge.ReactMethod;
7 | import com.facebook.react.bridge.ReadableArray;
8 | import com.facebook.react.bridge.ReadableMap;
9 | import com.facebook.react.common.MapBuilder;
10 | import com.facebook.react.uimanager.SimpleViewManager;
11 | import com.facebook.react.uimanager.ThemedReactContext;
12 | import com.facebook.react.uimanager.annotations.ReactProp;
13 |
14 | import java.util.Map;
15 |
16 | import javax.annotation.Nullable;
17 |
18 | public class ReactVlcPlayerViewManager extends SimpleViewManager {
19 |
20 | private static final String REACT_CLASS = "RCTVLCPlayer";
21 |
22 | private static final String PROP_SRC = "source";
23 | private static final String PROP_SRC_URI = "uri";
24 | private static final String PROP_SUBTITLE_URI = "subtitleUri";
25 | private static final String PROP_SRC_TYPE = "type";
26 | private static final String PROP_REPEAT = "repeat";
27 | private static final String PROP_PAUSED = "paused";
28 | private static final String PROP_MUTED = "muted";
29 | private static final String PROP_VOLUME = "volume";
30 | private static final String PROP_SEEK = "seek";
31 | private static final String PROP_RESUME = "resume";
32 | private static final String PROP_RATE = "rate";
33 | private static final String PROP_VIDEO_ASPECT_RATIO = "videoAspectRatio";
34 | private static final String PROP_SRC_IS_NETWORK = "isNetwork";
35 | private static final String PROP_AUTO_ASPECT_RATIO = "autoAspectRatio";
36 | private static final String PROP_CLEAR = "clear";
37 | private static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
38 | private static final String PROP_TEXT_TRACK = "textTrack";
39 | private static final String PROP_AUDIO_TRACK = "audioTrack";
40 | private static final String PROP_RECORDING_PATH = "recordingPath";
41 | private static final String PROP_ACCEPT_INVALID_CERTIFICATES = "acceptInvalidCertificates";
42 |
43 |
44 | @Override
45 | public String getName() {
46 | return REACT_CLASS;
47 | }
48 |
49 | @Override
50 | protected ReactVlcPlayerView createViewInstance(ThemedReactContext themedReactContext) {
51 | return new ReactVlcPlayerView(themedReactContext);
52 | }
53 |
54 | @Override
55 | public void onDropViewInstance(ReactVlcPlayerView view) {
56 | view.cleanUpResources();
57 | }
58 |
59 | @Override
60 | public @Nullable Map getExportedCustomDirectEventTypeConstants() {
61 | MapBuilder.Builder builder = MapBuilder.builder();
62 | for (String event : VideoEventEmitter.Events) {
63 | builder.put(event, MapBuilder.of("registrationName", event));
64 | }
65 | return builder.build();
66 | }
67 |
68 | @ReactProp(name = PROP_CLEAR)
69 | public void setClear(final ReactVlcPlayerView videoView, final boolean clear) {
70 | videoView.cleanUpResources();
71 | }
72 |
73 |
74 | @ReactProp(name = PROP_SRC)
75 | public void setSrc(final ReactVlcPlayerView videoView, @Nullable ReadableMap src) {
76 | Context context = videoView.getContext().getApplicationContext();
77 | String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null;
78 | String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
79 | boolean isNetStr = src.getBoolean(PROP_SRC_IS_NETWORK) ? src.getBoolean(PROP_SRC_IS_NETWORK) : false;
80 | boolean autoplay = src.getBoolean("autoplay") ? src.getBoolean("autoplay") : true;
81 | if (TextUtils.isEmpty(uriString)) {
82 | return;
83 | }
84 | videoView.setSrc(src);
85 |
86 | }
87 |
88 | @ReactProp(name = PROP_SUBTITLE_URI)
89 | public void setSubtitleUri(final ReactVlcPlayerView videoView, final String subtitleUri) {
90 | videoView.setSubtitleUri(subtitleUri);
91 | }
92 |
93 | @ReactProp(name = PROP_REPEAT, defaultBoolean = false)
94 | public void setRepeat(final ReactVlcPlayerView videoView, final boolean repeat) {
95 | videoView.setRepeatModifier(repeat);
96 | }
97 |
98 |
99 | @ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 0f )
100 | public void setInterval(final ReactVlcPlayerView videoView, final float interval) {
101 | videoView.setmProgressUpdateInterval(interval);
102 | }
103 |
104 | @ReactProp(name = PROP_PAUSED, defaultBoolean = false)
105 | public void setPaused(final ReactVlcPlayerView videoView, final boolean paused) {
106 | videoView.setPausedModifier(paused);
107 | }
108 |
109 | @ReactProp(name = PROP_MUTED, defaultBoolean = false)
110 | public void setMuted(final ReactVlcPlayerView videoView, final boolean muted) {
111 | videoView.setMutedModifier(muted);
112 | }
113 |
114 | @ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
115 | public void setVolume(final ReactVlcPlayerView videoView, final float volume) {
116 | videoView.setVolumeModifier((int)volume);
117 | }
118 |
119 |
120 | @ReactProp(name = PROP_SEEK)
121 | public void setSeek(final ReactVlcPlayerView videoView, final float seek) {
122 | videoView.setPosition(seek);
123 | }
124 |
125 | @ReactProp(name = PROP_AUTO_ASPECT_RATIO, defaultBoolean = false)
126 | public void setAutoAspectRatio(final ReactVlcPlayerView videoView, final boolean autoPlay) {
127 | videoView.setAutoAspectRatio(autoPlay);
128 | }
129 |
130 | @ReactProp(name = PROP_RESUME, defaultBoolean = true)
131 | public void setResume(final ReactVlcPlayerView videoView, final boolean autoPlay) {
132 | videoView.doResume(autoPlay);
133 | }
134 |
135 |
136 | @ReactProp(name = PROP_RATE)
137 | public void setRate(final ReactVlcPlayerView videoView, final float rate) {
138 | videoView.setRateModifier(rate);
139 | }
140 |
141 | @ReactProp(name = PROP_VIDEO_ASPECT_RATIO)
142 | public void setVideoAspectRatio(final ReactVlcPlayerView videoView, final String aspectRatio) {
143 | videoView.setAspectRatio(aspectRatio);
144 | }
145 |
146 | @ReactProp(name = PROP_AUDIO_TRACK)
147 | public void setAudioTrack(final ReactVlcPlayerView videoView, final int audioTrack) {
148 | videoView.setAudioTrack(audioTrack);
149 | }
150 |
151 | @ReactProp(name = PROP_TEXT_TRACK)
152 | public void setTextTrack(final ReactVlcPlayerView videoView, final int textTrack) {
153 | videoView.setTextTrack(textTrack);
154 | }
155 |
156 | @ReactProp(name = PROP_ACCEPT_INVALID_CERTIFICATES, defaultBoolean = false)
157 | public void setAcceptInvalidCertificates(final ReactVlcPlayerView videoView, final boolean accept) {
158 | videoView.setAcceptInvalidCertificates(accept);
159 | }
160 |
161 | public void startRecording(final ReactVlcPlayerView videoView, final String recordingPath) {
162 | videoView.startRecording(recordingPath);
163 | }
164 |
165 | public void stopRecording(final ReactVlcPlayerView videoView) {
166 | videoView.stopRecording();
167 | }
168 |
169 | public void stopPlayer(final ReactVlcPlayerView videoView) {
170 | videoView.stopPlayer();
171 | }
172 |
173 | public void snapshot(final ReactVlcPlayerView videoView, final String path) {
174 | videoView.doSnapshot(path);
175 | }
176 |
177 | @Override
178 | public Map getCommandsMap() {
179 | return MapBuilder.of(
180 | "startRecording", 1,
181 | "stopRecording", 2,
182 | "snapshot", 3
183 | );
184 | }
185 |
186 | @Override
187 | public void receiveCommand(ReactVlcPlayerView root, int commandId, @Nullable ReadableArray args) {
188 | switch (commandId) {
189 | case 1:
190 | if (args != null && args.size() > 0 && !args.isNull(0)) {
191 | String path = args.getString(0);
192 | root.startRecording(path);
193 | }
194 | break;
195 |
196 | case 2:
197 | root.stopRecording();
198 | break;
199 |
200 | case 3:
201 | if (args != null && args.size() > 0 && !args.isNull(0)) {
202 | String path = args.getString(0);
203 | root.doSnapshot(path);
204 | }
205 | break;
206 |
207 | default:
208 | break;
209 | }
210 | }
211 |
212 | private boolean startsWithValidScheme(String uriString) {
213 | return uriString.startsWith("http://")
214 | || uriString.startsWith("https://")
215 | || uriString.startsWith("content://")
216 | || uriString.startsWith("file://")
217 | || uriString.startsWith("asset://");
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/playerView/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by yuanzhou.xu on 2018/5/15.
3 | */
4 |
5 | import React, { Component } from 'react';
6 | import {
7 | StatusBar,
8 | View,
9 | StyleSheet,
10 | Platform,
11 | TouchableOpacity,
12 | Text,
13 | Dimensions,
14 | BackHandler,
15 | } from 'react-native';
16 |
17 | import VLCPlayerView from './VLCPlayerView';
18 | import PropTypes from 'prop-types';
19 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
20 | import { getStatusBarHeight } from './SizeController';
21 | const statusBarHeight = getStatusBarHeight();
22 | const _fullKey = 'commonVideo_android_fullKey';
23 | let deviceHeight = Dimensions.get('window').height;
24 | let deviceWidth = Dimensions.get('window').width;
25 | export default class CommonVideo extends Component {
26 | constructor(props) {
27 | super(props);
28 | this.url = '';
29 | this.initialHeight = 200;
30 |
31 | if (props.widthCamera) {
32 | deviceWidth = props.widthCamera
33 | }
34 | }
35 |
36 | static navigationOptions = {
37 | header: null,
38 | };
39 |
40 | state = {
41 | isEndGG: false,
42 | isFull: false,
43 | currentUrl: '',
44 | storeUrl: '',
45 | };
46 |
47 | static defaultProps = {
48 | height: 250,
49 | showGG: false,
50 | ggUrl: '',
51 | url: '',
52 | showBack: false,
53 | showTitle: false,
54 | };
55 |
56 | static propTypes = {
57 | /**
58 | * 视频播放错误
59 | */
60 | onError: PropTypes.func,
61 | /**
62 | * 视频播放结束
63 | */
64 | onEnd: PropTypes.func,
65 |
66 | /**
67 | * 广告头播放结束
68 | */
69 | onGGEnd: PropTypes.func,
70 | /**
71 | * 开启全屏
72 | */
73 | startFullScreen: PropTypes.func,
74 | /**
75 | * 关闭全屏
76 | */
77 | closeFullScreen: PropTypes.func,
78 | /**
79 | * 返回按钮点击事件
80 | */
81 | onLeftPress: PropTypes.func,
82 | /**
83 | * 标题
84 | */
85 | title: PropTypes.string,
86 | /**
87 | * 是否显示返回按钮
88 | */
89 | showBack: PropTypes.bool,
90 | /**
91 | * 是否显示标题
92 | */
93 | showTitle: PropTypes.bool,
94 |
95 | onGoLivePress: PropTypes.func,
96 |
97 | onReplayPress: PropTypes.func,
98 | };
99 |
100 | static getDerivedStateFromProps(nextProps, preState) {
101 | let { url } = nextProps;
102 | let { currentUrl, storeUrl } = preState;
103 | if (url && url !== storeUrl) {
104 | if (storeUrl === "") {
105 | return {
106 | currentUrl: url,
107 | storeUrl: url,
108 | isEndGG: false,
109 | };
110 | } else {
111 | return {
112 | currentUrl: "",
113 | storeUrl: url,
114 | isEndGG: false,
115 | };
116 | }
117 | }
118 | return null;
119 | }
120 |
121 |
122 | componentDidUpdate(prevProps, prevState) {
123 | if (this.props.url !== prevState.storeUrl && this._componentMounted) {
124 | this.setState({
125 | storeUrl: this.props.url,
126 | currentUrl: this.props.url
127 | })
128 | }
129 | }
130 |
131 | componentDidMount() {
132 | this._componentMounted = true
133 | StatusBar.setBarStyle("light-content");
134 | let { style, isGG } = this.props;
135 |
136 | if (style && style.height && !isNaN(style.height)) {
137 | this.initialHeight = style.height;
138 | }
139 | this.setState({
140 | currentVideoAspectRatio: deviceWidth + ":" + this.initialHeight,
141 | });
142 |
143 | let { isFull } = this.props;
144 | console.log(`isFull == ${isFull}`);
145 | if (isFull) {
146 | this._toFullScreen();
147 | }
148 | }
149 |
150 | componentWillUnmount() {
151 | this._componentMounted = false;
152 |
153 | let { isFull } = this.props;
154 | if (isFull) {
155 | this._closeFullScreen();
156 | }
157 | }
158 |
159 | _closeFullScreen = () => {
160 | let { closeFullScreen, BackHandle, Orientation } = this.props;
161 | if (this._componentMounted) {
162 | this.setState({ isFull: false, currentVideoAspectRatio: deviceWidth + ":" + this.initialHeight, });
163 | }
164 | BackHandle && BackHandle.removeBackFunction(_fullKey);
165 | Orientation && Orientation.lockToPortrait;
166 | StatusBar.setHidden(false);
167 | //StatusBar.setTranslucent(false);
168 | this._componentMounted && closeFullScreen && closeFullScreen();
169 | };
170 |
171 | _toFullScreen = () => {
172 | let { startFullScreen, BackHandle, Orientation } = this.props;
173 | //StatusBar.setTranslucent(true);
174 | this.setState({ isFull: true, currentVideoAspectRatio: deviceHeight + ":" + deviceWidth, });
175 | StatusBar.setHidden(true);
176 | BackHandle && BackHandle.addBackFunction(_fullKey, this._closeFullScreen);
177 | startFullScreen && startFullScreen();
178 | Orientation && Orientation.lockToLandscape && Orientation.lockToLandscape;
179 | };
180 |
181 | _onLayout = (e) => {
182 | let { width, height } = e.nativeEvent.layout;
183 | console.log(e.nativeEvent.layout);
184 | if (width * height > 0) {
185 | this.width = width;
186 | this.height = height;
187 | if (!this.initialHeight) {
188 | this.initialHeight = height;
189 | }
190 | }
191 | }
192 |
193 | render() {
194 | let { url, ggUrl, showGG, onGGEnd, onEnd, onError, style, height, title, onLeftPress, showBack, showTitle, closeFullScreen, videoAspectRatio, fullVideoAspectRatio } = this.props;
195 | let { isEndGG, isFull, currentUrl } = this.state;
196 | let currentVideoAspectRatio = '';
197 | if (isFull) {
198 | currentVideoAspectRatio = fullVideoAspectRatio;
199 | } else {
200 | currentVideoAspectRatio = videoAspectRatio;
201 | }
202 | if (!currentVideoAspectRatio) {
203 | let { width, height } = this.state;
204 | currentVideoAspectRatio = this.state.currentVideoAspectRatio;
205 | }
206 | let realShowGG = false;
207 | let type = '';
208 | let ggType = '';
209 | let showVideo = false;
210 | let showTop = false;
211 | if (showGG && ggUrl && !isEndGG) {
212 | realShowGG = true;
213 | }
214 | if (currentUrl) {
215 | if (!showGG || (showGG && isEndGG)) {
216 | showVideo = true;
217 | }
218 | if (currentUrl.split) {
219 | let types = currentUrl.split('.');
220 | if (types && types.length > 0) {
221 | type = types[types.length - 1];
222 | }
223 | }
224 | }
225 | if (ggUrl && ggUrl.split) {
226 | let types = ggUrl.split('.');
227 | if (types && types.length > 0) {
228 | ggType = types[types.length - 1];
229 | }
230 | }
231 | if (!showVideo && !realShowGG) {
232 | showTop = true;
233 | }
234 | return (
235 |
238 | {showTop &&
239 |
240 | {showBack && {
242 | if (isFull) {
243 | closeFullScreen && closeFullScreen();
244 | } else {
245 | onLeftPress && onLeftPress();
246 | }
247 | }}
248 | style={styles.btn}
249 | activeOpacity={0.8}>
250 |
251 |
252 | }
253 |
254 | {showTitle &&
255 | {title}
256 | }
257 |
258 |
259 |
260 | }
261 | {realShowGG && (
262 | {
273 | onGGEnd && onGGEnd();
274 | this.setState({ isEndGG: true });
275 | }}
276 | startFullScreen={this._toFullScreen}
277 | closeFullScreen={this._closeFullScreen}
278 | />
279 | )}
280 |
281 | {showVideo && (
282 | {
300 | onEnd && onEnd();
301 | }}
302 | onError={() => {
303 | onError && onError();
304 | }}
305 | />
306 | )}
307 |
308 | );
309 | }
310 | }
311 |
312 | const styles = StyleSheet.create({
313 | container: {
314 | flex: 1,
315 | backgroundColor: '#000'
316 | },
317 | topView: {
318 | top: Platform.OS === 'ios' ? statusBarHeight : 0,
319 | left: 0,
320 | height: 45,
321 | position: 'absolute',
322 | width: '100%'
323 | },
324 | backBtn: {
325 | height: 45,
326 | width: '100%',
327 | flexDirection: 'row',
328 | alignItems: 'center'
329 | },
330 | btn: {
331 | marginLeft: 10,
332 | marginRight: 10,
333 | justifyContent: 'center',
334 | alignItems: 'center',
335 | backgroundColor: 'rgba(0,0,0,0.1)',
336 | height: 40,
337 | borderRadius: 20,
338 | width: 40,
339 | }
340 | });
341 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0C1A0ECA1D07E18700441684 /* RCTVLCPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A0EC81D07E18700441684 /* RCTVLCPlayerManager.m */; };
11 | 0CA30C481D07E0DB003B09F9 /* RCTVLCPlayer.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0CA30C471D07E0DB003B09F9 /* RCTVLCPlayer.h */; };
12 | 0CA30C4A1D07E0DB003B09F9 /* RCTVLCPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CA30C491D07E0DB003B09F9 /* RCTVLCPlayer.m */; };
13 | /* End PBXBuildFile section */
14 |
15 | /* Begin PBXCopyFilesBuildPhase section */
16 | 0CA30C421D07E0DB003B09F9 /* CopyFiles */ = {
17 | isa = PBXCopyFilesBuildPhase;
18 | buildActionMask = 2147483647;
19 | dstPath = "include/$(PRODUCT_NAME)";
20 | dstSubfolderSpec = 16;
21 | files = (
22 | 0CA30C481D07E0DB003B09F9 /* RCTVLCPlayer.h in CopyFiles */,
23 | );
24 | runOnlyForDeploymentPostprocessing = 0;
25 | };
26 | /* End PBXCopyFilesBuildPhase section */
27 |
28 | /* Begin PBXFileReference section */
29 | 0C1A0EC81D07E18700441684 /* RCTVLCPlayerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVLCPlayerManager.m; sourceTree = ""; };
30 | 0C1A0EC91D07E18700441684 /* RCTVLCPlayerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVLCPlayerManager.h; sourceTree = ""; };
31 | 0CA30C441D07E0DB003B09F9 /* libRCTVLCPlayer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVLCPlayer.a; sourceTree = BUILT_PRODUCTS_DIR; };
32 | 0CA30C471D07E0DB003B09F9 /* RCTVLCPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTVLCPlayer.h; sourceTree = ""; };
33 | 0CA30C491D07E0DB003B09F9 /* RCTVLCPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTVLCPlayer.m; sourceTree = ""; };
34 | /* End PBXFileReference section */
35 |
36 | /* Begin PBXFrameworksBuildPhase section */
37 | 0CA30C411D07E0DB003B09F9 /* Frameworks */ = {
38 | isa = PBXFrameworksBuildPhase;
39 | buildActionMask = 2147483647;
40 | files = (
41 | );
42 | runOnlyForDeploymentPostprocessing = 0;
43 | };
44 | /* End PBXFrameworksBuildPhase section */
45 |
46 | /* Begin PBXGroup section */
47 | 0CA30C3B1D07E0DB003B09F9 = {
48 | isa = PBXGroup;
49 | children = (
50 | 0CA30C461D07E0DB003B09F9 /* RCTVLCPlayer */,
51 | 0CA30C451D07E0DB003B09F9 /* Products */,
52 | );
53 | sourceTree = "";
54 | };
55 | 0CA30C451D07E0DB003B09F9 /* Products */ = {
56 | isa = PBXGroup;
57 | children = (
58 | 0CA30C441D07E0DB003B09F9 /* libRCTVLCPlayer.a */,
59 | );
60 | name = Products;
61 | sourceTree = "";
62 | };
63 | 0CA30C461D07E0DB003B09F9 /* RCTVLCPlayer */ = {
64 | isa = PBXGroup;
65 | children = (
66 | 0C1A0EC81D07E18700441684 /* RCTVLCPlayerManager.m */,
67 | 0C1A0EC91D07E18700441684 /* RCTVLCPlayerManager.h */,
68 | 0CA30C471D07E0DB003B09F9 /* RCTVLCPlayer.h */,
69 | 0CA30C491D07E0DB003B09F9 /* RCTVLCPlayer.m */,
70 | );
71 | path = RCTVLCPlayer;
72 | sourceTree = "";
73 | };
74 | /* End PBXGroup section */
75 |
76 | /* Begin PBXNativeTarget section */
77 | 0CA30C431D07E0DB003B09F9 /* RCTVLCPlayer */ = {
78 | isa = PBXNativeTarget;
79 | buildConfigurationList = 0CA30C4D1D07E0DB003B09F9 /* Build configuration list for PBXNativeTarget "RCTVLCPlayer" */;
80 | buildPhases = (
81 | 0CA30C401D07E0DB003B09F9 /* Sources */,
82 | 0CA30C411D07E0DB003B09F9 /* Frameworks */,
83 | 0CA30C421D07E0DB003B09F9 /* CopyFiles */,
84 | );
85 | buildRules = (
86 | );
87 | dependencies = (
88 | );
89 | name = RCTVLCPlayer;
90 | productName = RCTVLCPlayer;
91 | productReference = 0CA30C441D07E0DB003B09F9 /* libRCTVLCPlayer.a */;
92 | productType = "com.apple.product-type.library.static";
93 | };
94 | /* End PBXNativeTarget section */
95 |
96 | /* Begin PBXProject section */
97 | 0CA30C3C1D07E0DB003B09F9 /* Project object */ = {
98 | isa = PBXProject;
99 | attributes = {
100 | LastUpgradeCheck = 0730;
101 | ORGANIZATIONNAME = "熊川";
102 | TargetAttributes = {
103 | 0CA30C431D07E0DB003B09F9 = {
104 | CreatedOnToolsVersion = 7.3.1;
105 | };
106 | };
107 | };
108 | buildConfigurationList = 0CA30C3F1D07E0DB003B09F9 /* Build configuration list for PBXProject "RCTVLCPlayer" */;
109 | compatibilityVersion = "Xcode 3.2";
110 | developmentRegion = English;
111 | hasScannedForEncodings = 0;
112 | knownRegions = (
113 | English,
114 | en,
115 | );
116 | mainGroup = 0CA30C3B1D07E0DB003B09F9;
117 | productRefGroup = 0CA30C451D07E0DB003B09F9 /* Products */;
118 | projectDirPath = "";
119 | projectRoot = "";
120 | targets = (
121 | 0CA30C431D07E0DB003B09F9 /* RCTVLCPlayer */,
122 | );
123 | };
124 | /* End PBXProject section */
125 |
126 | /* Begin PBXSourcesBuildPhase section */
127 | 0CA30C401D07E0DB003B09F9 /* Sources */ = {
128 | isa = PBXSourcesBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | 0C1A0ECA1D07E18700441684 /* RCTVLCPlayerManager.m in Sources */,
132 | 0CA30C4A1D07E0DB003B09F9 /* RCTVLCPlayer.m in Sources */,
133 | );
134 | runOnlyForDeploymentPostprocessing = 0;
135 | };
136 | /* End PBXSourcesBuildPhase section */
137 |
138 | /* Begin XCBuildConfiguration section */
139 | 0CA30C4B1D07E0DB003B09F9 /* Debug */ = {
140 | isa = XCBuildConfiguration;
141 | buildSettings = {
142 | ALWAYS_SEARCH_USER_PATHS = NO;
143 | CLANG_ANALYZER_NONNULL = YES;
144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
145 | CLANG_CXX_LIBRARY = "libc++";
146 | CLANG_ENABLE_MODULES = YES;
147 | CLANG_ENABLE_OBJC_ARC = YES;
148 | CLANG_WARN_BOOL_CONVERSION = YES;
149 | CLANG_WARN_CONSTANT_CONVERSION = YES;
150 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
151 | CLANG_WARN_EMPTY_BODY = YES;
152 | CLANG_WARN_ENUM_CONVERSION = YES;
153 | CLANG_WARN_INT_CONVERSION = YES;
154 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
155 | CLANG_WARN_UNREACHABLE_CODE = YES;
156 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
157 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
158 | COPY_PHASE_STRIP = NO;
159 | DEBUG_INFORMATION_FORMAT = dwarf;
160 | ENABLE_STRICT_OBJC_MSGSEND = YES;
161 | ENABLE_TESTABILITY = YES;
162 | GCC_C_LANGUAGE_STANDARD = gnu99;
163 | GCC_DYNAMIC_NO_PIC = NO;
164 | GCC_NO_COMMON_BLOCKS = YES;
165 | GCC_OPTIMIZATION_LEVEL = 0;
166 | GCC_PREPROCESSOR_DEFINITIONS = (
167 | "DEBUG=1",
168 | "$(inherited)",
169 | );
170 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
171 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
172 | GCC_WARN_UNDECLARED_SELECTOR = YES;
173 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
174 | GCC_WARN_UNUSED_FUNCTION = YES;
175 | GCC_WARN_UNUSED_VARIABLE = YES;
176 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
177 | MTL_ENABLE_DEBUG_INFO = YES;
178 | ONLY_ACTIVE_ARCH = YES;
179 | SDKROOT = iphoneos;
180 | VALID_ARCHS = "arm64 armv7 armv7s";
181 | };
182 | name = Debug;
183 | };
184 | 0CA30C4C1D07E0DB003B09F9 /* Release */ = {
185 | isa = XCBuildConfiguration;
186 | buildSettings = {
187 | ALWAYS_SEARCH_USER_PATHS = NO;
188 | CLANG_ANALYZER_NONNULL = YES;
189 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
190 | CLANG_CXX_LIBRARY = "libc++";
191 | CLANG_ENABLE_MODULES = YES;
192 | CLANG_ENABLE_OBJC_ARC = YES;
193 | CLANG_WARN_BOOL_CONVERSION = YES;
194 | CLANG_WARN_CONSTANT_CONVERSION = YES;
195 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
196 | CLANG_WARN_EMPTY_BODY = YES;
197 | CLANG_WARN_ENUM_CONVERSION = YES;
198 | CLANG_WARN_INT_CONVERSION = YES;
199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
200 | CLANG_WARN_UNREACHABLE_CODE = YES;
201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
202 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
205 | ENABLE_NS_ASSERTIONS = NO;
206 | ENABLE_STRICT_OBJC_MSGSEND = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu99;
208 | GCC_NO_COMMON_BLOCKS = YES;
209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
211 | GCC_WARN_UNDECLARED_SELECTOR = YES;
212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
213 | GCC_WARN_UNUSED_FUNCTION = YES;
214 | GCC_WARN_UNUSED_VARIABLE = YES;
215 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
216 | MTL_ENABLE_DEBUG_INFO = NO;
217 | SDKROOT = iphoneos;
218 | VALIDATE_PRODUCT = YES;
219 | VALID_ARCHS = "arm64 armv7 armv7s";
220 | };
221 | name = Release;
222 | };
223 | 0CA30C4E1D07E0DB003B09F9 /* Debug */ = {
224 | isa = XCBuildConfiguration;
225 | buildSettings = {
226 | FRAMEWORK_SEARCH_PATHS = (
227 | "$(SRCROOT)/../../../vlcKit",
228 | "\"$(SRCROOT)/../../../ios/Pods/MobileVLCKit-unstable/MobileVLCKit-binary\"",
229 | );
230 | HEADER_SEARCH_PATHS = (
231 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
232 | "$(inherited)",
233 | "$(SRCROOT)/../../react-native/React/**",
234 | "$(SRCROOT)/node_modules/react-native/React/**",
235 | "$(SRCROOT)/../../../ios/Pods/MobileVLCKit-unstable/MobileVLCKit-binary/MobileVLCKit.framework",
236 | );
237 | ONLY_ACTIVE_ARCH = YES;
238 | OTHER_LDFLAGS = "-ObjC";
239 | PRODUCT_NAME = "$(TARGET_NAME)";
240 | SKIP_INSTALL = YES;
241 | VALID_ARCHS = "arm64 armv7 armv7s";
242 | };
243 | name = Debug;
244 | };
245 | 0CA30C4F1D07E0DB003B09F9 /* Release */ = {
246 | isa = XCBuildConfiguration;
247 | buildSettings = {
248 | FRAMEWORK_SEARCH_PATHS = (
249 | "$(SRCROOT)/../../../vlcKit",
250 | "\"$(SRCROOT)/../../../ios/Pods/MobileVLCKit-unstable/MobileVLCKit-binary\"",
251 | );
252 | HEADER_SEARCH_PATHS = (
253 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
254 | "$(inherited)",
255 | "$(SRCROOT)/../../react-native/React/**",
256 | "$(SRCROOT)/node_modules/react-native/React/**",
257 | "$(SRCROOT)/../../../ios/Pods/MobileVLCKit-unstable/MobileVLCKit-binary/MobileVLCKit.framework",
258 | );
259 | ONLY_ACTIVE_ARCH = NO;
260 | OTHER_LDFLAGS = "-ObjC";
261 | PRODUCT_NAME = "$(TARGET_NAME)";
262 | SKIP_INSTALL = YES;
263 | VALID_ARCHS = "arm64 armv7 armv7s";
264 | };
265 | name = Release;
266 | };
267 | /* End XCBuildConfiguration section */
268 |
269 | /* Begin XCConfigurationList section */
270 | 0CA30C3F1D07E0DB003B09F9 /* Build configuration list for PBXProject "RCTVLCPlayer" */ = {
271 | isa = XCConfigurationList;
272 | buildConfigurations = (
273 | 0CA30C4B1D07E0DB003B09F9 /* Debug */,
274 | 0CA30C4C1D07E0DB003B09F9 /* Release */,
275 | );
276 | defaultConfigurationIsVisible = 0;
277 | defaultConfigurationName = Release;
278 | };
279 | 0CA30C4D1D07E0DB003B09F9 /* Build configuration list for PBXNativeTarget "RCTVLCPlayer" */ = {
280 | isa = XCConfigurationList;
281 | buildConfigurations = (
282 | 0CA30C4E1D07E0DB003B09F9 /* Debug */,
283 | 0CA30C4F1D07E0DB003B09F9 /* Release */,
284 | );
285 | defaultConfigurationIsVisible = 0;
286 | defaultConfigurationName = Release;
287 | };
288 | /* End XCConfigurationList section */
289 | };
290 | rootObject = 0CA30C3C1D07E0DB003B09F9 /* Project object */;
291 | }
292 |
--------------------------------------------------------------------------------
/android/react-native-yz-vlcplayer.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-vlc-media-player
2 |
3 | ## Supported RN Versions
4 |
5 | - 0.59 > 0.62 and up
6 | - PODs are updated to work with 0.61 and up (tested in 0.61.5, 0.62 and 0.63)
7 |
8 | ## Supported formats
9 |
10 | Support for network streams, RTSP, RTP, RTMP, HLS, MMS.
11 | Play all files, [in all formats, including exotic ones, like the classic VLC media player.](#-More-formats)
12 | Play MKV, multiple audio tracks (including 5.1), and subtitles tracks (including SSA!)
13 |
14 | ## Sample repo
15 |
16 | [VLC Media Player test](https://github.com/razorRun/react-native-vlc-media-player-test)
17 |
18 | ## Add it to your project
19 |
20 | Run
21 |
22 | `npm i react-native-vlc-media-player --save`
23 |
24 | or
25 |
26 | `yarn add react-native-vlc-media-player`
27 |
28 | If not using Expo also run
29 |
30 | `react-native link react-native-vlc-media-player`
31 |
32 | ## Android
33 |
34 | Should work without any specific settings. Gradle build might fail with `More than one file was found with OS independent path 'lib/x86/libc++_shared.so'` error.
35 |
36 | If that happens, add the following block to your `android/app/build.gradle`:
37 |
38 | ```gradle
39 | tasks.whenTaskAdded((tas -> {
40 | // when task is 'mergeLocalDebugNativeLibs' or 'mergeLocalReleaseNativeLibs'
41 | if (tas.name.contains("merge") && tas.name.contains("NativeLibs")) {
42 | tasks.named(tas.name) {it
43 | doFirst {
44 | java.nio.file.Path notNeededDirectory = it.externalLibNativeLibs
45 | .getFiles()
46 | .stream()
47 | // for React Native 0.71, the file value now contains "jetified-react-android" instead of "jetified-react-native"
48 | .filter(file -> file.toString().contains("jetified-react-native"))
49 | .findAny()
50 | .orElse(null)
51 | .toPath();
52 | java.nio.file.Files.walk(notNeededDirectory).forEach(file -> {
53 | if (file.toString().contains("libc++_shared.so")) {
54 | java.nio.file.Files.delete(file);
55 | }
56 | });
57 | }
58 | }
59 | }
60 | }))
61 | ```
62 |
63 | ### Explanation
64 | `react-native` and `LibVLC` both import `libc++_shared.so`, but we cannot use `packagingOptions.pickFirst` to handle this case, because `libvlc-all:3.6.0-eap5` will crash when using `libc++_shared.so`, so we have to use `libc++_shared.so` from `LibVLC`.
65 |
66 | Reference: https://stackoverflow.com/questions/74258902/how-to-define-which-so-file-to-use-in-gradle-packaging-options
67 |
68 | ### Also to consider
69 | `libvlc-all:3.2.6` has a bug where subtitles won't display on Android 12 and 13, so we have to upgrade `LibVLC` to support it.
70 |
71 | Reference: https://code.videolan.org/videolan/vlc-android/-/issues/2252
72 |
73 | ## iOS
74 |
75 | 1. cd to ios
76 | 2. run `pod init` (if only Podfile has not been generated in ios folder)
77 | 3. add `pod 'MobileVLCKit', '3.3.10'` to pod file **(No need if you are running RN 0.61 and up)**
78 | 4. run `pod install` (you have to delete the app on the simulator/device and run `react-native run-ios` again)
79 |
80 | ### Important
81 |
82 | Starting from iOS 14, you are required to provide a message for the `NSLocalNetworkUsageDescription` key in `Info.plist` if your app uses the local network directly or indirectly.
83 |
84 | It seems the `MobileVLCKit` library powering the VLC Player on iOS makes use of this feature when playing external media from sources such as RTSP streams from IP cameras.
85 |
86 | Provide a custom message specifying how your app will make use of the network so your App Store submission is not rejected for this reason, read more about this here:
87 |
88 | https://developer.apple.com/documentation/bundleresources/information-property-list/nslocalnetworkusagedescription
89 |
90 | ### Optional
91 |
92 | In root project select "Build Settings", find "Bitcode" and select "Enable Bitcode"
93 |
94 | ## Expo
95 |
96 | This package works with Expo, Expo Go does not include custom native code so you must use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
97 |
98 | To enable just insert the `react-native-vlc-media-player` plugin to the "plugins" array from `app.config.js` or `app.json`:
99 |
100 | ```json
101 | {
102 | "expo": {
103 | "plugins": [
104 | [
105 | "react-native-vlc-media-player",
106 | {
107 | "ios": {
108 | "includeVLCKit": false
109 | },
110 | "android": {
111 | "legacyJetifier": false
112 | }
113 | }
114 | ]
115 | ],
116 | }
117 | }
118 | ```
119 |
120 | ### Configuring the plugin is optional:
121 |
122 | - Set `ios.includeVLCKit` to `true` if using RN < 0.61
123 | - Set `android.legacyJetifier` to `true` if using RN < 0.71
124 |
125 | Then rebuild your app as described in the ["Adding custom native code"](https://docs.expo.io/workflow/customizing/) guide.
126 |
127 | ## Usage
128 |
129 | ```jsx
130 | import { VLCPlayer, VlCPlayerView } from 'react-native-vlc-media-player';
131 | import Orientation from 'react-native-orientation';
132 |
133 |
138 |
139 | // Or you can use
140 |
141 | {}}
151 | />
152 | ```
153 |
154 | ### VLCPlayer Props
155 |
156 | | Prop | Description | Default |
157 | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
158 | | `source` | Object that contains the uri of a video or song to play eg `{{ uri: "https://video.com/example.mkv" }}` | `{}` |
159 | | `subtitleUri` | local subtitle file path,if you want to hide subtitle, you can set this to an empty subtitle file,current we don't support a `hide subtitle` prop. | |
160 | | `paused` | Set to `true` or `false` to pause or play the media | `false` |
161 | | `repeat` | Set to `true` or `false` to loop the media | `false` |
162 | | `rate` | Set the playback rate of the player | `1` |
163 | | `seek` | Set position to seek between `0` and `1` (`0` being the start, `1` being the end , use `position` from the progress object ) | |
164 | | `volume` | Set the volume of the player (`number`) | |
165 | | `muted` | Set to `true` or `false` to mute the player | `false` |
166 | | `audioTrack` | Set audioTrack id (`number`) (see `onLoad` callback VideoInfo.audioTracks) | |
167 | | `textTrack` | Set textTrack(subtitle) id (`number`) (see `onLoad` callback- VideoInfo.textTracks) | |
168 | | `playInBackground` | Set to `true` or `false` to allow playing in the background | false |
169 | | `videoAspectRatio ` | Set the video aspect ratio eg `"16:9"` | |
170 | | `autoAspectRatio` | Set to `true` or `false` to enable auto aspect ratio | false |
171 | | `resizeMode` | Set the behavior for the video size (`fill, contain, cover, none, scale-down`) | none |
172 | | `style` | React native stylesheet styles | `{}` |
173 | | `autoplay` | Set to `true` or `false` to enable autoplay | `true` |
174 |
175 | #### Callback props
176 |
177 | Callback props take a function that gets fired on various player events:
178 |
179 | | Prop | Description |
180 | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181 | | `onPlaying` | Called when media starts playing returns eg `{target: 9, duration: 99750, seekable: true}` |
182 | | `onProgress` | Callback containing `position` as a fraction, and `duration`, `currentTime` and `remainingTime` in seconds
◦ eg `{ duration: 99750, position: 0.30, currentTime: 30154, remainingTime: -69594 }` |
183 | | `onPaused` | Called when media is paused |
184 | | `onStopped ` | Called when media is stoped |
185 | | `onBuffering ` | Called when media is buffering |
186 | | `onEnded` | Called when media playing ends |
187 | | `onError` | Called when an error occurs whilst attempting to play media |
188 | | `onLoad` | Called when video info is loaded, Callback containing VideoInfo |
189 | | `onRecordingCreated` | Called when a new recording is created as the result of `startRecording()` `stopRecording()` |
190 | | `onSnapshot` | Called when a new snapshot is created as the result of `snapshot()` - contains `{success: boolean, path?: string, error?: string}` |
191 |
192 | #### Methods props
193 |
194 | Methods available on the VLC player ref
195 |
196 | | Prop | Description |
197 | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
198 | | `startRecording(directory: string)` | Start recording the current video into the given directory |
199 | | `stopRecording()` | Stop recording the current video. The final recording file can be obtained from the `onRecordingCreated` callback |
200 | | `snapshot(path: string)` | Capture a snapshot of the current video frame to the given file path |
201 |
202 | VideoInfo example:
203 |
204 | ```
205 | {
206 | duration: 30906,
207 | videoSize: {height: 240, width: 32},
208 | audioTracks: [
209 | {id: -1, name: "Disable"},
210 | {id: 1, name: "Track 1"},
211 | {id: 3, name: "Japanese Audio (2ch LC-AAC) - [Japanese]"}
212 | ],
213 | textTracks: [
214 | {id: -1, name: "Disable"},
215 | {id: 4, name: "Track 1 - [English]"},
216 | {id: 5, name: "Track 2 - [Japanese]"}
217 | ],
218 | }
219 | ```
220 |
221 | ## More formats
222 |
223 | Container formats: 3GP, ASF, AVI, DVR-MS, FLV, Matroska (MKV), MIDI, QuickTime File Format, MP4, Ogg, OGM, WAV, MPEG-2 (ES, PS, TS, PVA, MP3), AIFF, Raw audio, Raw DV, MXF, VOB, RM, Blu-ray, DVD-Video, VCD, SVCD, CD Audio, DVB, HEIF, AVIF
224 | Audio coding formats: AAC, AC3, ALAC, AMR, DTS, DV Audio, XM, FLAC, It, MACE, MOD, Monkey's Audio, MP3, Opus, PLS, QCP, QDM2/QDMC, RealAudio, Speex, Screamtracker 3/S3M, TTA, Vorbis, WavPack, WMA (WMA 1/2, WMA 3 partially).
225 | Capture devices: Video4Linux (on Linux), DirectShow (on Windows), Desktop (screencast), Digital TV (DVB-C, DVB-S, DVB-T, DVB-S2, DVB-T2, ATSC, Clear QAM)
226 | Network protocols: FTP, HTTP, MMS, RSS/Atom, RTMP, RTP (unicast or multicast), RTSP, UDP, Sat-IP, Smooth Streaming
227 | Network streaming formats: Apple HLS, Flash RTMP, MPEG-DASH, MPEG Transport Stream, RTP/RTSP ISMA/3GPP PSS, Windows Media MMS
228 | Subtitles: Advanced SubStation Alpha, Closed Captions, DVB, DVD-Video, MPEG-4 Timed Text, MPL2, OGM, SubStation Alpha, SubRip, SVCD, Teletext, Text file, VobSub, WebVTT, TTML
229 | Video coding formats: Cinepak, Dirac, DV, H.263, H.264/MPEG-4 AVC, H.265/MPEG HEVC, AV1, HuffYUV, Indeo 3, MJPEG, MPEG-1, MPEG-2, MPEG-4 Part 2, RealVideo 3&4, Sorenson, Theora, VC-1,[h] VP5, VP6, VP8, VP9, DNxHD, ProRes and some WMV.
230 |
231 | ## Got a few minutes to spare? Please help us to keep this repo up to date and simple to use.
232 |
233 | #### Our idea was to keep the repo simple, and people can use it with newer RN versions without any additional config.
234 |
235 | 1. Get a fork of this repo and clone [VLC Media Player test](https://github.com/razorRun/react-native-vlc-media-player-test)
236 | 2. Run it for ios and android locally using your fork, and do the changes. (remove this package using `npm remove react-native-vlc-media-player` and install the forked version from git hub `npm i https://git-address-to-your-forked-repo`)
237 | 3. Verify your changes and make sure everything works on both platforms. (If you need a hand with testing I might be able to help as well)
238 | 4. Send PR.
239 | 5. Be happy, Cause you're a Rockstar 🌟 ❤️
240 |
241 | ## Known Issues
242 |
243 | ### iOS 17 Simulator Crash
244 |
245 | It is a [known issue](https://code.videolan.org/videolan/VLCKit/-/issues/724) that apps can crash on playback in iOS simulator with `EXEC_BAD_ACCESS` errors. This appears to only be on certain iOS 17.x versions (17.4, 17.5).
246 | If this happens, try running on an iOS 18+ simulator instead.
247 |
248 | ## TODO
249 |
250 | 1. Android video aspect ratio and other params do not work (Events are called but all events come through a single event onVideoStateChange but the JS side does not implement it)
251 |
252 | ## Credits
253 |
254 | [cornejobarraza](https://github.com/cornejobarraza)
255 | [ammarahm-ed](https://github.com/ammarahm-ed)
256 | [Nghi-NV](https://github.com/Nghi-NV)
257 | [xuyuanzhou](https://github.com/xuyuanzhou)
258 |
259 |
260 | Author - Roshan Milinda -> [roshan.digital](https://roshan.digital)
261 |
--------------------------------------------------------------------------------
/playerView/VLCPlayerView.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by yuanzhou.xu on 2018/5/14.
3 | */
4 | import React, { Component } from 'react';
5 | import {
6 | StyleSheet,
7 | Text,
8 | View,
9 | Dimensions,
10 | TouchableOpacity,
11 | ActivityIndicator,
12 | StatusBar,
13 | BackHandler,
14 | Modal,
15 | Platform,
16 | } from 'react-native';
17 | import VLCPlayer from '../VLCPlayer';
18 | import PropTypes from 'prop-types';
19 | import TimeLimt from './TimeLimit';
20 | import ControlBtn from './ControlBtn';
21 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
22 | import { getStatusBarHeight } from './SizeController';
23 | const statusBarHeight = getStatusBarHeight();
24 | let deviceHeight = Dimensions.get('window').height;
25 | let deviceWidth = Dimensions.get('window').width;
26 | export default class VLCPlayerView extends Component {
27 | static propTypes = {
28 | uri: PropTypes.string,
29 | };
30 |
31 | constructor(props) {
32 | super(props);
33 | this.state = {
34 | paused: true,
35 | isLoading: true,
36 | loadingSuccess: false,
37 | isFull: false,
38 | currentTime: 0.0,
39 | totalTime: 0.0,
40 | showControls: false,
41 | seek: 0,
42 | isError: false,
43 | };
44 | this.touchTime = 0;
45 | this.changeUrl = false;
46 | this.isEnding = false;
47 | this.reloadSuccess = false;
48 | }
49 |
50 | static defaultProps = {
51 | initPaused: false,
52 | source: null,
53 | seek: 0,
54 | playInBackground: false,
55 | isGG: false,
56 | autoplay: true,
57 | errorTitle: 'error'
58 | };
59 |
60 | componentDidMount() {
61 | if (this.props.isFull) {
62 | this.setState({
63 | showControls: true,
64 | });
65 | }
66 | }
67 |
68 | componentWillUnmount() {
69 | this.vlcPlayer._onStopped()
70 |
71 | if (this.bufferInterval) {
72 | clearInterval(this.bufferInterval);
73 | this.bufferInterval = null;
74 | }
75 |
76 | }
77 |
78 | componentDidUpdate(prevProps, prevState) {
79 | if (this.props.uri !== prevProps.uri) {
80 | console.log("componentDidUpdate");
81 | this.changeUrl = true;
82 | }
83 | }
84 |
85 | render() {
86 | let {
87 | onEnd,
88 | onError,
89 | style,
90 | isGG,
91 | type,
92 | isFull,
93 | uri,
94 | title,
95 | onLeftPress,
96 | closeFullScreen,
97 | showBack,
98 | showTitle,
99 | videoAspectRatio,
100 | showGoLive,
101 | onGoLivePress,
102 | onReplayPress,
103 | titleGolive,
104 | showLeftButton,
105 | showMiddleButton,
106 | showRightButton,
107 | errorTitle
108 | } = this.props;
109 | let { isLoading, loadingSuccess, showControls, isError } = this.state;
110 | let showGG = false;
111 | let realShowLoding = false;
112 | let source = {};
113 | if (uri) {
114 | if (uri.split) {
115 | source = { uri: this.props.uri };
116 | } else {
117 | source = uri;
118 | }
119 | }
120 | if (Platform.OS === 'ios') {
121 | if ((loadingSuccess && isGG) || (isGG && type === 'swf')) {
122 | showGG = true;
123 | }
124 | if (isLoading && type !== 'swf') {
125 | realShowLoding = true;
126 | }
127 | } else {
128 | if (loadingSuccess && isGG) {
129 | showGG = true;
130 | }
131 | if (isLoading) {
132 | realShowLoding = true;
133 | }
134 | }
135 |
136 | return (
137 | {
141 | let currentTime = new Date().getTime();
142 | if (this.touchTime === 0) {
143 | this.touchTime = currentTime;
144 | this.setState({ showControls: !this.state.showControls });
145 | } else {
146 | if (currentTime - this.touchTime >= 500) {
147 | this.touchTime = currentTime;
148 | this.setState({ showControls: !this.state.showControls });
149 | }
150 | }
151 | }}>
152 | (this.vlcPlayer = ref)}
154 | paused={this.state.paused}
155 | //seek={this.state.seek}
156 | style={[styles.video]}
157 | source={source}
158 | videoAspectRatio={videoAspectRatio}
159 | onProgress={this.onProgress.bind(this)}
160 | onEnd={this.onEnded.bind(this)}
161 | //onEnded={this.onEnded.bind(this)}
162 | onStopped={this.onEnded.bind(this)}
163 | onPlaying={this.onPlaying.bind(this)}
164 | onBuffering={this.onBuffering.bind(this)}
165 | onPaused={this.onPaused.bind(this)}
166 | progressUpdateInterval={250}
167 | onError={this._onError}
168 | // onError={this.onError.bind(this)}
169 | onOpen={this._onOpen}
170 | onLoadStart={this._onLoadStart}
171 | />
172 | {realShowLoding &&
173 | !isError && (
174 |
175 |
176 |
177 | )}
178 | {isError && (
179 |
180 | {errorTitle}
181 |
190 |
191 |
192 |
193 | )}
194 |
195 |
196 | {showBack && (
197 | {
199 | if (isFull) {
200 | closeFullScreen && closeFullScreen();
201 | } else {
202 | onLeftPress && onLeftPress();
203 | }
204 | }}
205 | style={styles.btn}
206 | activeOpacity={0.8}>
207 |
208 |
209 | )}
210 |
211 | {showTitle &&
212 | showControls && (
213 |
214 | {title}
215 |
216 | )}
217 |
218 | {showGG && (
219 |
220 | {
222 | onEnd && onEnd();
223 | }}
224 | //maxTime={Math.ceil(this.state.totalTime)}
225 | />
226 |
227 | )}
228 |
229 |
230 |
231 | {showControls && (
232 | {
246 | this.changingSlider = true;
247 | this.setState({
248 | currentTime: value,
249 | });
250 | }}
251 | onSlidingComplete={value => {
252 | this.changingSlider = false;
253 | if (Platform.OS === 'ios') {
254 | this.vlcPlayer.seek(Number((value / this.state.totalTime).toFixed(17)));
255 | } else {
256 | this.vlcPlayer.seek(value);
257 | }
258 | }}
259 | showGoLive={showGoLive}
260 | onGoLivePress={onGoLivePress}
261 | onReplayPress={onReplayPress}
262 | titleGolive={titleGolive}
263 | showLeftButton={showLeftButton}
264 | showMiddleButton={showMiddleButton}
265 | showRightButton={showRightButton}
266 | />
267 | )}
268 |
269 |
270 | );
271 | }
272 |
273 | /**
274 | * 视屏播放
275 | * @param event
276 | */
277 | onPlaying(event) {
278 | this.isEnding = false;
279 | // if (this.state.paused) {
280 | // this.setState({ paused: false });
281 | // }
282 | console.log('onPlaying');
283 | }
284 |
285 | /**
286 | * 视屏停止
287 | * @param event
288 | */
289 | onPaused(event) {
290 | // if (!this.state.paused) {
291 | // this.setState({ paused: true, showControls: true });
292 | // } else {
293 | // this.setState({ showControls: true });
294 | // }
295 | console.log('onPaused');
296 | }
297 |
298 | /**
299 | * 视屏缓冲
300 | * @param event
301 | */
302 | onBuffering(event) {
303 | this.setState({
304 | isLoading: true,
305 | isError: false,
306 | });
307 | this.bufferTime = new Date().getTime();
308 | if (!this.bufferInterval) {
309 | this.bufferInterval = setInterval(this.bufferIntervalFunction, 250);
310 | }
311 | console.log('onBuffering');
312 | console.log(event);
313 | }
314 |
315 | bufferIntervalFunction = () => {
316 | console.log('bufferIntervalFunction');
317 | let currentTime = new Date().getTime();
318 | let diffTime = currentTime - this.bufferTime;
319 | if (diffTime > 1000) {
320 | clearInterval(this.bufferInterval);
321 | this.setState({
322 | paused: true,
323 | }, () => {
324 | this.setState({
325 | paused: false,
326 | isLoading: false,
327 | });
328 | });
329 | this.bufferInterval = null;
330 | console.log('remove bufferIntervalFunction');
331 | }
332 | };
333 |
334 | _onError = e => {
335 | // [bavv add start]
336 | let { onVLCError, onError } = this.props;
337 | onVLCError && onVLCError();
338 | // [bavv add end]
339 | console.log('_onError');
340 | console.log(e);
341 | this.reloadSuccess = false;
342 | this.setState({
343 | isError: true,
344 | });
345 | onError&&onError()
346 | };
347 |
348 | _onOpen = e => {
349 | console.log('onOpen', e);
350 | };
351 |
352 | _onLoadStart = e => {
353 | console.log('_onLoadStart');
354 | console.log(e);
355 | let { isError } = this.state;
356 | if (isError) {
357 | this.reloadSuccess = true;
358 | let { currentTime, totalTime } = this.state;
359 | if (Platform.OS === 'ios') {
360 | this.vlcPlayer.seek(Number((currentTime / totalTime).toFixed(17)));
361 | } else {
362 | this.vlcPlayer.seek(currentTime);
363 | }
364 | this.setState({
365 | paused: true,
366 | isError: false,
367 | }, () => {
368 | this.setState({
369 | paused: false,
370 | });
371 | })
372 | } else {
373 | this.vlcPlayer.seek(0);
374 | this.setState({
375 | isLoading: true,
376 | isError: false,
377 | loadingSuccess: false,
378 | paused: true,
379 | currentTime: 0.0,
380 | totalTime: 0.0,
381 | }, () => {
382 | this.setState({
383 | paused: false,
384 | });
385 | })
386 | }
387 | };
388 |
389 | _reload = () => {
390 | if (!this.reloadSuccess) {
391 | this.vlcPlayer.resume && this.vlcPlayer.resume(false);
392 | }
393 | };
394 |
395 | /**
396 | * 视屏进度变化
397 | * @param event
398 | */
399 | onProgress(event) {
400 | /* console.log(
401 | 'position=' +
402 | event.position +
403 | ',currentTime=' +
404 | event.currentTime +
405 | ',remainingTime=' +
406 | event.remainingTime,
407 | );*/
408 | let currentTime = event.currentTime;
409 | let loadingSuccess = false;
410 | if (currentTime > 0 || this.state.currentTime > 0) {
411 | loadingSuccess = true;
412 | }
413 | if (!this.changingSlider) {
414 | if (currentTime === 0 || currentTime === this.state.currentTime * 1000) {
415 | } else {
416 | this.setState({
417 | loadingSuccess: loadingSuccess,
418 | isLoading: false,
419 | isError: false,
420 | progress: event.position,
421 | currentTime: event.currentTime / 1000,
422 | totalTime: event.duration / 1000,
423 | });
424 | }
425 | }
426 | }
427 |
428 | /**
429 | * 视屏播放结束
430 | * @param event
431 | */
432 | onEnded(event) {
433 | console.log('onEnded ---------->')
434 | console.log(event)
435 | console.log('<---------- onEnded ')
436 | let { currentTime, totalTime } = this.state;
437 | // [bavv add start]
438 | let { onVLCEnded, onEnd, autoplay, isGG } = this.props;
439 | onVLCEnded && onVLCEnded();
440 | // [bavv add end]
441 | if (((currentTime + 5) >= totalTime && totalTime > 0) || isGG) {
442 | this.setState(
443 | {
444 | paused: true,
445 | //showControls: true,
446 | },
447 | () => {
448 | if (!this.isEnding) {
449 | onEnd && onEnd();
450 | if (!isGG) {
451 | this.vlcPlayer.resume && this.vlcPlayer.resume(false);
452 | console.log(this.props.uri + ': onEnded');
453 | } else {
454 | console.log('片头:' + this.props.uri + ': onEnded');
455 | }
456 | this.isEnding = true;
457 | }
458 | },
459 | );
460 | } else {
461 | /* console.log('onEnded error:'+this.props.uri);
462 | this.vlcPlayer.resume && this.vlcPlayer.resume(false);*/
463 | /*this.setState({
464 | paused: true,
465 | },()=>{
466 | console.log('onEnded error:'+this.props.uri);
467 | this.reloadSuccess = false;
468 | this.setState({
469 | isError: true,
470 | });
471 | });*/
472 | }
473 | }
474 |
475 | /**
476 | * 全屏
477 | * @private
478 | */
479 | _toFullScreen = () => {
480 | let { startFullScreen, closeFullScreen, isFull } = this.props;
481 | if (isFull) {
482 | closeFullScreen && closeFullScreen();
483 | } else {
484 | startFullScreen && startFullScreen();
485 | }
486 | };
487 |
488 | /**
489 | * 播放/停止
490 | * @private
491 | */
492 | _play = () => {
493 | this.setState({ paused: !this.state.paused });
494 | };
495 | }
496 |
497 | const styles = StyleSheet.create({
498 | container: {
499 | flex: 1,
500 | },
501 | videoBtn: {
502 | flex: 1,
503 | },
504 | video: {
505 | justifyContent: 'center',
506 | alignItems: 'center',
507 | height: '100%',
508 | width: '100%',
509 | },
510 | loading: {
511 | position: 'absolute',
512 | left: 0,
513 | top: 0,
514 | zIndex: 0,
515 | width: '100%',
516 | height: '100%',
517 | justifyContent: 'center',
518 | alignItems: 'center',
519 | },
520 | GG: {
521 | backgroundColor: 'rgba(255,255,255,1)',
522 | height: 30,
523 | marginRight: 10,
524 | paddingLeft: 10,
525 | paddingRight: 10,
526 | borderRadius: 20,
527 | justifyContent: 'center',
528 | alignItems: 'center',
529 | },
530 | topView: {
531 | top: Platform.OS === 'ios' ? statusBarHeight : 0,
532 | left: 0,
533 | height: 45,
534 | position: 'absolute',
535 | width: '100%',
536 | //backgroundColor: 'red'
537 | },
538 | bottomView: {
539 | bottom: 0,
540 | left: 0,
541 | height: 50,
542 | position: 'absolute',
543 | width: '100%',
544 | backgroundColor: 'rgba(0,0,0,0)',
545 | },
546 | backBtn: {
547 | height: 45,
548 | width: '100%',
549 | flexDirection: 'row',
550 | alignItems: 'center',
551 | },
552 | btn: {
553 | marginLeft: 10,
554 | marginRight: 10,
555 | justifyContent: 'center',
556 | alignItems: 'center',
557 | backgroundColor: 'rgba(0,0,0,0.3)',
558 | height: 40,
559 | borderRadius: 20,
560 | width: 40,
561 | paddingTop: 3,
562 | },
563 | });
564 |
--------------------------------------------------------------------------------
/ios/RCTVLCPlayer/RCTVLCPlayer.m:
--------------------------------------------------------------------------------
1 | #import "React/RCTConvert.h"
2 | #import "RCTVLCPlayer.h"
3 | #import "React/RCTBridgeModule.h"
4 | #import "React/RCTEventDispatcher.h"
5 | #import "React/UIView+React.h"
6 | #if TARGET_OS_TV
7 | #import
8 | #else
9 | #import
10 | #endif
11 | #import
12 | static NSString *const statusKeyPath = @"status";
13 | static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp";
14 | static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty";
15 | static NSString *const readyForDisplayKeyPath = @"readyForDisplay";
16 | static NSString *const playbackRate = @"rate";
17 |
18 |
19 | #if !defined(DEBUG) || !(TARGET_IPHONE_SIMULATOR)
20 | #define NSLog(...)
21 | #endif
22 |
23 |
24 | @implementation RCTVLCPlayer
25 | {
26 |
27 | /* Required to publish events */
28 | RCTEventDispatcher *_eventDispatcher;
29 | VLCMediaPlayer *_player;
30 |
31 | NSDictionary * _videoInfo;
32 | NSString * _subtitleUri;
33 |
34 | BOOL _paused;
35 | BOOL _autoplay;
36 | BOOL _acceptInvalidCertificates;
37 | }
38 |
39 | - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
40 | {
41 | if ((self = [super init])) {
42 | _eventDispatcher = eventDispatcher;
43 |
44 | [[NSNotificationCenter defaultCenter] addObserver:self
45 | selector:@selector(applicationWillResignActive:)
46 | name:UIApplicationWillResignActiveNotification
47 | object:nil];
48 |
49 | [[NSNotificationCenter defaultCenter] addObserver:self
50 | selector:@selector(applicationWillEnterForeground:)
51 | name:UIApplicationWillEnterForegroundNotification
52 | object:nil];
53 |
54 | }
55 |
56 | return self;
57 | }
58 |
59 | - (void)applicationWillEnterForeground:(NSNotification *)notification
60 | {
61 | if (!_paused)
62 | [self play];
63 | }
64 |
65 | - (void)applicationWillResignActive:(NSNotification *)notification
66 | {
67 | if (!_paused)
68 | [self play];
69 | }
70 |
71 | - (void)play
72 | {
73 | if (_player) {
74 | [_player play];
75 | _paused = NO;
76 | }
77 | }
78 |
79 | - (void)pause
80 | {
81 | if (_player) {
82 | [_player pause];
83 | _paused = YES;
84 | }
85 | }
86 |
87 | - (void)setSource:(NSDictionary *)source
88 | {
89 | if (_player) {
90 | [self _release];
91 | }
92 |
93 | _videoInfo = nil;
94 |
95 | // [bavv edit start]
96 | NSString* uriString = [source objectForKey:@"uri"];
97 | NSURL* uri = [NSURL URLWithString:uriString];
98 | int initType = [source objectForKey:@"initType"];
99 | NSDictionary* initOptions = [source objectForKey:@"initOptions"];
100 |
101 | // Get acceptInvalidCertificates from source
102 | _acceptInvalidCertificates = [[source objectForKey:@"acceptInvalidCertificates"] boolValue];
103 | NSLog(@"iOS: Set acceptInvalidCertificates to %@", _acceptInvalidCertificates ? @"YES" : @"NO");
104 |
105 | if (initType == 1) {
106 | _player = [[VLCMediaPlayer alloc] init];
107 | } else {
108 | _player = [[VLCMediaPlayer alloc] initWithOptions:initOptions];
109 | }
110 | _player.delegate = self;
111 | _player.drawable = self;
112 | // [bavv edit end]
113 |
114 | VLCLibrary *library = _player.libraryInstance;
115 |
116 | VLCConsoleLogger *consoleLogger = [[VLCConsoleLogger alloc] init];
117 | consoleLogger.level = kVLCLogLevelDebug;
118 | library.loggers = @[consoleLogger];
119 |
120 | // Create dialog provider with custom UI to handle dialogs programmatically
121 | self.dialogProvider = [[VLCDialogProvider alloc] initWithLibrary:library customUI:YES];
122 | self.dialogProvider.customRenderer = self;
123 | _player.media = [VLCMedia mediaWithURL:uri];
124 |
125 | if (_autoplay)
126 | [_player play];
127 |
128 | [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
129 | }
130 |
131 | - (void)setAutoplay:(BOOL)autoplay
132 | {
133 | _autoplay = autoplay;
134 |
135 | if (autoplay)
136 | [self play];
137 | }
138 |
139 | - (void)setPaused:(BOOL)paused
140 | {
141 | _paused = paused;
142 |
143 | if (!paused) {
144 | [self play];
145 | } else {
146 | [self pause];
147 | }
148 | }
149 |
150 | - (void)setResume:(BOOL)resume
151 | {
152 | if (resume) {
153 | [self play];
154 | } else {
155 | [self pause];
156 | }
157 | }
158 |
159 | - (void)setSubtitleUri:(NSString *)subtitleUri
160 | {
161 | NSURL *url = [NSURL URLWithString:subtitleUri];
162 |
163 | if (url.absoluteString.length != 0 && _player) {
164 | _subtitleUri = url;
165 | [_player addPlaybackSlave:_subtitleUri type:VLCMediaPlaybackSlaveTypeSubtitle enforce:YES];
166 | } else {
167 | NSLog(@"Invalid subtitle URI: %@", subtitleUri);
168 | }
169 | }
170 |
171 | // ==== player delegate methods ====
172 |
173 | - (void)mediaPlayerTimeChanged:(NSNotification *)aNotification
174 | {
175 | [self updateVideoProgress];
176 | }
177 |
178 | - (void)mediaPlayerStateChanged:(NSNotification *)aNotification
179 | {
180 |
181 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
182 | NSLog(@"userInfo %@",[aNotification userInfo]);
183 | NSLog(@"standardUserDefaults %@",defaults);
184 | if (_player) {
185 | VLCMediaPlayerState state = _player.state;
186 | switch (state) {
187 | case VLCMediaPlayerStateOpening:
188 | NSLog(@"VLCMediaPlayerStateOpening %i", _player.numberOfAudioTracks);
189 | self.onVideoOpen(@{
190 | @"target": self.reactTag
191 | });
192 | self.onVideoLoadStart(@{
193 | @"target": self.reactTag
194 | });
195 | break;
196 | case VLCMediaPlayerStatePaused:
197 | _paused = YES;
198 | NSLog(@"VLCMediaPlayerStatePaused %i", _player.numberOfAudioTracks);
199 | self.onVideoPaused(@{
200 | @"target": self.reactTag
201 | });
202 | break;
203 | case VLCMediaPlayerStateStopped:
204 | NSLog(@"VLCMediaPlayerStateStopped %i", _player.numberOfAudioTracks);
205 | self.onVideoStopped(@{
206 | @"target": self.reactTag
207 | });
208 | break;
209 | case VLCMediaPlayerStateBuffering:
210 | NSLog(@"VLCMediaPlayerStateBuffering %i", _player.numberOfAudioTracks);
211 | self.onVideoBuffering(@{
212 | @"target": self.reactTag
213 | });
214 | break;
215 | case VLCMediaPlayerStatePlaying:
216 | _paused = NO;
217 | NSLog(@"VLCMediaPlayerStatePlaying %i", _player.numberOfAudioTracks);
218 | self.onVideoPlaying(@{
219 | @"target": self.reactTag,
220 | @"seekable": [NSNumber numberWithBool:[_player isSeekable]],
221 | @"duration":[NSNumber numberWithInt:[_player.media.length intValue]]
222 | });
223 | break;
224 | case VLCMediaPlayerStateEnded:
225 | NSLog(@"VLCMediaPlayerStateEnded %i", _player.numberOfAudioTracks);
226 | int currentTime = [[_player time] intValue];
227 | int remainingTime = [[_player remainingTime] intValue];
228 | int duration = [_player.media.length intValue];
229 |
230 | self.onVideoEnded(@{
231 | @"target": self.reactTag,
232 | @"currentTime": [NSNumber numberWithInt:currentTime],
233 | @"remainingTime": [NSNumber numberWithInt:remainingTime],
234 | @"duration":[NSNumber numberWithInt:duration],
235 | @"position":[NSNumber numberWithFloat:_player.position]
236 | });
237 | break;
238 | case VLCMediaPlayerStateError:
239 | NSLog(@"VLCMediaPlayerStateError %i", _player.numberOfAudioTracks);
240 | // This callback doesn't have any data about the error, we need to rely on the error dialog
241 | [self _release];
242 | break;
243 | default:
244 | break;
245 | }
246 | }
247 | }
248 |
249 |
250 | // ===== media delegate methods =====
251 |
252 | - (void)mediaDidFinishParsing:(VLCMedia *)aMedia {
253 | NSLog(@"VLCMediaDidFinishParsing %i", _player.numberOfAudioTracks);
254 | }
255 |
256 | - (void)mediaMetaDataDidChange:(VLCMedia *)aMedia{
257 | NSLog(@"VLCMediaMetaDataDidChange %i", _player.numberOfAudioTracks);
258 | }
259 |
260 | - (void)mediaPlayer:(VLCMediaPlayer *)player recordingStoppedAtPath:(NSString *)path {
261 | if (self.onRecordingState) {
262 | self.onRecordingState(@{
263 | @"target": self.reactTag,
264 | @"isRecording": @NO,
265 | @"recordPath": path ?: [NSNull null]
266 | });
267 | }
268 | }
269 |
270 | // ===================================
271 |
272 | - (void)updateVideoProgress
273 | {
274 | if (_player && !_paused) {
275 | int currentTime = [[_player time] intValue];
276 | int remainingTime = [[_player remainingTime] intValue];
277 | int duration = [_player.media.length intValue];
278 | [self updateVideoInfo];
279 |
280 | self.onVideoProgress(@{
281 | @"target": self.reactTag,
282 | @"currentTime": [NSNumber numberWithInt:currentTime],
283 | @"remainingTime": [NSNumber numberWithInt:remainingTime],
284 | @"duration":[NSNumber numberWithInt:duration],
285 | @"position":[NSNumber numberWithFloat:_player.position],
286 | });
287 | }
288 | }
289 |
290 | - (void)updateVideoInfo
291 | {
292 | NSMutableDictionary *info = [NSMutableDictionary new];
293 | info[@"duration"] = _player.media.length.value;
294 | int i;
295 | if (_player.videoSize.width > 0) {
296 | info[@"videoSize"] = @{
297 | @"width": @(_player.videoSize.width),
298 | @"height": @(_player.videoSize.height)
299 | };
300 | }
301 |
302 | if (_player.numberOfAudioTracks > 0) {
303 | NSMutableArray *tracks = [NSMutableArray new];
304 | for (i = 0; i < _player.numberOfAudioTracks; i++) {
305 | if (_player.audioTrackIndexes[i] && _player.audioTrackNames[i]) {
306 | [tracks addObject: @{
307 | @"id": _player.audioTrackIndexes[i],
308 | @"name": _player.audioTrackNames[i]
309 | }];
310 | }
311 | }
312 | info[@"audioTracks"] = tracks;
313 | }
314 |
315 | if (_player.numberOfSubtitlesTracks > 0) {
316 | NSMutableArray *tracks = [NSMutableArray new];
317 | for (i = 0; i < _player.numberOfSubtitlesTracks; i++) {
318 | if (_player.videoSubTitlesIndexes[i] && _player.videoSubTitlesNames[i]) {
319 | [tracks addObject: @{
320 | @"id": _player.videoSubTitlesIndexes[i],
321 | @"name": _player.videoSubTitlesNames[i]
322 | }];
323 | }
324 | }
325 | info[@"textTracks"] = tracks;
326 | }
327 |
328 | if (![_videoInfo isEqualToDictionary:info]) {
329 | self.onVideoLoad(info);
330 | _videoInfo = info;
331 | }
332 | }
333 |
334 | - (void)jumpBackward:(int)interval
335 | {
336 | if (interval>=0 && interval <= [_player.media.length intValue])
337 | [_player jumpBackward:interval];
338 | }
339 |
340 | - (void)jumpForward:(int)interval
341 | {
342 | if (interval>=0 && interval <= [_player.media.length intValue])
343 | [_player jumpForward:interval];
344 | }
345 |
346 | - (void)setSeek:(float)pos
347 | {
348 | if ([_player isSeekable]) {
349 | if (pos>=0 && pos <= 1) {
350 | [_player setPosition:pos];
351 | }
352 | }
353 | }
354 |
355 | - (void)setSnapshotPath:(NSString*)path
356 | {
357 | if (_player)
358 | [_player saveVideoSnapshotAt:path withWidth:0 andHeight:0];
359 | }
360 |
361 | - (void)setRate:(float)rate
362 | {
363 | [_player setRate:rate];
364 | }
365 |
366 | - (void)setAudioTrack:(int)track
367 | {
368 | [_player setCurrentAudioTrackIndex: track];
369 | }
370 |
371 | - (void)setTextTrack:(int)track
372 | {
373 | [_player setCurrentVideoSubTitleIndex:track];
374 | }
375 |
376 | - (void)startRecording:(NSString*)path
377 | {
378 | [_player startRecordingAtPath:path];
379 | if (self.onRecordingState) {
380 | self.onRecordingState(@{
381 | @"target": self.reactTag,
382 | @"isRecording": @YES
383 | });
384 | }
385 | }
386 |
387 | - (void)stopRecording
388 | {
389 | [_player stopRecording];
390 | }
391 |
392 | - (void)stopPlayer
393 | {
394 | [_player stop];
395 | }
396 |
397 | - (void)snapshot:(NSString*)path
398 | {
399 | @try {
400 | if (_player) {
401 | [_player saveVideoSnapshotAt:path withWidth:_player.videoSize.width andHeight:_player.videoSize.height];
402 | self.onSnapshot(@{
403 | @"success": @YES,
404 | @"path": path,
405 | @"error": [NSNull null],
406 | @"target": self.reactTag
407 | });
408 | } else {
409 | @throw [NSException exceptionWithName:@"PlayerNotInitialized" reason:@"Player is not initialized" userInfo:nil];
410 | }
411 | } @catch (NSException *e) {
412 | NSLog(@"Error in snapshot: %@", e);
413 | self.onSnapshot(@{
414 | @"success": @NO,
415 | @"error": [e description],
416 | @"target": self.reactTag
417 | });
418 | }
419 | }
420 |
421 | - (void)setVideoAspectRatio:(NSString *)ratio{
422 | char *char_content = [ratio cStringUsingEncoding:NSASCIIStringEncoding];
423 | [_player setVideoAspectRatio:char_content];
424 | }
425 |
426 | - (void)setMuted:(BOOL)value
427 | {
428 | if (_player) {
429 | [[_player audio] setMuted:value];
430 | }
431 | }
432 |
433 | #pragma mark - VLCCustomDialogRendererProtocol
434 |
435 | - (void)showErrorWithTitle:(NSString *)title message:(NSString *)message {
436 | NSLog(@"VLC Error - Title: %@, Message: %@", title, message);
437 | if (self.onVideoError) {
438 | self.onVideoError(@{
439 | @"target": self.reactTag,
440 | @"title": title ?: [NSNull null],
441 | @"message": message ?: [NSNull null]
442 | });
443 | }
444 | }
445 |
446 | - (void)showLoginWithTitle:(NSString *)title
447 | message:(NSString *)message
448 | defaultUsername:(NSString *)username
449 | askingForStorage:(BOOL)askingForStorage
450 | withReference:(NSValue *)reference {
451 | NSLog(@"VLC Login - Title: %@, Message: %@", title, message);
452 | if (self.onVideoError) {
453 | self.onVideoError(@{
454 | @"target": self.reactTag,
455 | @"title": title ?: [NSNull null],
456 | @"message": message ?: [NSNull null]
457 | });
458 | }
459 | }
460 |
461 | - (void)showQuestionWithTitle:(NSString *)title
462 | message:(NSString *)message
463 | type:(VLCDialogQuestionType)type
464 | cancelString:(NSString *)cancel
465 | action1String:(NSString *)action1
466 | action2String:(NSString *)action2
467 | withReference:(NSValue *)reference {
468 |
469 | NSLog(@"VLC Question - Title: %@, Message: %@", title, message);
470 |
471 | // Check if this is a certificate-related dialog
472 | NSString *fullText = [NSString stringWithFormat:@"%@ %@", title ?: @"", message ?: @""];
473 | BOOL isCertificateDialog = [fullText containsString:@"certificate"] ||
474 | [fullText containsString:@"SSL"] ||
475 | [fullText containsString:@"TLS"] ||
476 | [fullText containsString:@"cert"] ||
477 | [fullText containsString:@"security"];
478 |
479 | if (isCertificateDialog) {
480 | if (_acceptInvalidCertificates) {
481 | // Accept certificate (usually action1)
482 | [self.dialogProvider postAction:1 forDialogReference:reference];
483 | NSLog(@"iOS: Auto-accepted certificate dialog");
484 | } else {
485 | // Reject certificate (cancel)
486 | [self.dialogProvider postAction:3 forDialogReference:reference]; // Cancel
487 | NSLog(@"iOS: Rejected certificate dialog");
488 | }
489 | } else {
490 | // For other dialogs, default to cancel
491 | [self.dialogProvider postAction:3 forDialogReference:reference];
492 | }
493 | }
494 |
495 | - (void)showProgressWithTitle:(NSString *)title
496 | message:(NSString *)message
497 | isIndeterminate:(BOOL)indeterminate
498 | position:(float)position
499 | cancelString:(NSString *)cancel
500 | withReference:(NSValue *)reference {
501 | NSLog(@"VLC Progress - Title: %@, Message: %@, Position: %.2f", title, message, position);
502 | // Handle progress dialog if needed
503 | }
504 |
505 | - (void)updateProgressWithReference:(NSValue *)reference
506 | message:(NSString *)message
507 | position:(float)position {
508 | // Update progress dialog
509 | }
510 |
511 | - (void)cancelDialogWithReference:(NSValue *)reference {
512 | NSLog(@"VLC Dialog cancelled");
513 | // Handle dialog cancellation
514 | }
515 |
516 | - (void)setAcceptInvalidCertificates:(BOOL)accept
517 | {
518 | _acceptInvalidCertificates = accept;
519 | NSLog(@"iOS: Set acceptInvalidCertificates to %@", accept ? @"YES" : @"NO");
520 | }
521 |
522 | - (void)_release
523 | {
524 | [[NSNotificationCenter defaultCenter] removeObserver:self];
525 |
526 | if (_player.media)
527 | [_player stop];
528 |
529 | if (_player)
530 | _player = nil;
531 |
532 | _eventDispatcher = nil;
533 | }
534 |
535 |
536 | #pragma mark - Lifecycle
537 | - (void)removeFromSuperview
538 | {
539 | NSLog(@"removeFromSuperview");
540 | [self _release];
541 | [super removeFromSuperview];
542 | }
543 |
544 | @end
--------------------------------------------------------------------------------
/android/src/main/java/com/yuanzhou/vlc/vlcplayer/ReactVlcPlayerView.java:
--------------------------------------------------------------------------------
1 | package com.yuanzhou.vlc.vlcplayer;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.graphics.SurfaceTexture;
7 | import android.media.AudioManager;
8 | import android.net.Uri;
9 | import android.os.Handler;
10 | import android.os.Looper;
11 | import android.util.DisplayMetrics;
12 | import android.util.Log;
13 | import android.view.Surface;
14 | import android.view.TextureView;
15 | import android.view.View;
16 | import com.facebook.react.bridge.Arguments;
17 | import com.facebook.react.bridge.LifecycleEventListener;
18 | import com.facebook.react.bridge.ReadableArray;
19 | import com.facebook.react.bridge.ReadableMap;
20 | import com.facebook.react.bridge.WritableMap;
21 | import com.facebook.react.bridge.WritableNativeArray;
22 | import com.facebook.react.bridge.WritableArray;
23 |
24 | import com.facebook.react.uimanager.ThemedReactContext;
25 |
26 | import org.videolan.libvlc.interfaces.IVLCVout;
27 | import org.videolan.libvlc.LibVLC;
28 | import org.videolan.libvlc.Media;
29 | import org.videolan.libvlc.MediaPlayer;
30 | import org.videolan.libvlc.Dialog;
31 |
32 | import java.io.File;
33 | import java.io.FileOutputStream;
34 | import java.util.ArrayList;
35 |
36 |
37 | @SuppressLint("ViewConstructor")
38 | class ReactVlcPlayerView extends TextureView implements
39 | LifecycleEventListener,
40 | TextureView.SurfaceTextureListener,
41 | AudioManager.OnAudioFocusChangeListener {
42 |
43 | private static final String TAG = "ReactVlcPlayerView";
44 | private final String tag = "ReactVlcPlayerView";
45 |
46 | private final VideoEventEmitter eventEmitter;
47 | private LibVLC libvlc;
48 | private MediaPlayer mMediaPlayer = null;
49 | private boolean mMuted = false;
50 | private boolean isSurfaceViewDestory;
51 | private String src;
52 | private String _subtitleUri;
53 | private boolean netStrTag;
54 | private ReadableMap srcMap;
55 | private int mVideoHeight = 0;
56 | private TextureView surfaceView;
57 | private Surface surfaceVideo;
58 | private int mVideoWidth = 0;
59 | private int mVideoVisibleHeight = 0;
60 | private int mVideoVisibleWidth = 0;
61 | private int mSarNum = 0;
62 | private int mSarDen = 0;
63 | private int screenWidth = 0;
64 | private int screenHeight = 0;
65 |
66 | private boolean isPaused = true;
67 | private boolean isHostPaused = false;
68 | private int preVolume = 100;
69 | private boolean autoAspectRatio = false;
70 | private boolean acceptInvalidCertificates = false;
71 |
72 | private float mProgressUpdateInterval = 0;
73 | private Handler mProgressUpdateHandler = new Handler();
74 | private Runnable mProgressUpdateRunnable = null;
75 |
76 | private final ThemedReactContext themedReactContext;
77 | private final AudioManager audioManager;
78 |
79 | private WritableMap mVideoInfo = null;
80 | private String mVideoInfoHash = null;
81 |
82 |
83 | public ReactVlcPlayerView(ThemedReactContext context) {
84 | super(context);
85 | this.eventEmitter = new VideoEventEmitter(context);
86 | this.themedReactContext = context;
87 | audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
88 | DisplayMetrics dm = getResources().getDisplayMetrics();
89 | screenHeight = dm.heightPixels;
90 | screenWidth = dm.widthPixels;
91 | this.setSurfaceTextureListener(this);
92 |
93 | this.addOnLayoutChangeListener(onLayoutChangeListener);
94 | context.addLifecycleEventListener(this);
95 | }
96 |
97 |
98 | @Override
99 | public void setId(int id) {
100 | super.setId(id);
101 | eventEmitter.setViewId(id);
102 | }
103 |
104 | @Override
105 | protected void onAttachedToWindow() {
106 | super.onAttachedToWindow();
107 | //createPlayer();
108 | }
109 |
110 | @Override
111 | protected void onDetachedFromWindow() {
112 | super.onDetachedFromWindow();
113 | stopPlayback();
114 | }
115 |
116 | // LifecycleEventListener implementation
117 |
118 | @Override
119 | public void onHostResume() {
120 | if (mMediaPlayer != null && isSurfaceViewDestory && isHostPaused) {
121 | IVLCVout vlcOut = mMediaPlayer.getVLCVout();
122 | if (!vlcOut.areViewsAttached()) {
123 | // vlcOut.setVideoSurface(this.getHolder().getSurface(), this.getHolder());
124 | vlcOut.attachViews(onNewVideoLayoutListener);
125 | isSurfaceViewDestory = false;
126 | isPaused = false;
127 | // this.getHolder().setKeepScreenOn(true);
128 | mMediaPlayer.play();
129 | }
130 | }
131 | }
132 |
133 |
134 | @Override
135 | public void onHostPause() {
136 | if (!isPaused && mMediaPlayer != null) {
137 | isPaused = true;
138 | isHostPaused = true;
139 | mMediaPlayer.pause();
140 | // this.getHolder().setKeepScreenOn(false);
141 | WritableMap map = Arguments.createMap();
142 | map.putString("type", "Paused");
143 | eventEmitter.onVideoStateChange(map);
144 | }
145 | Log.i("onHostPause", "---------onHostPause------------>");
146 | }
147 |
148 |
149 | @Override
150 | public void onHostDestroy() {
151 | stopPlayback();
152 | }
153 |
154 |
155 | // AudioManager.OnAudioFocusChangeListener implementation
156 | @Override
157 | public void onAudioFocusChange(int focusChange) {
158 | }
159 |
160 | private void setProgressUpdateRunnable() {
161 | if (mMediaPlayer != null && mProgressUpdateInterval > 0){
162 | new Thread() {
163 | @Override
164 | public void run() {
165 | super.run();
166 |
167 | mProgressUpdateRunnable = new Runnable() {
168 | @Override
169 | public void run() {
170 | if (mMediaPlayer != null && !isPaused) {
171 | long currentTime = 0;
172 | long totalLength = 0;
173 | WritableMap event = Arguments.createMap();
174 | boolean isPlaying = mMediaPlayer.isPlaying();
175 | currentTime = mMediaPlayer.getTime();
176 | float position = mMediaPlayer.getPosition();
177 | totalLength = mMediaPlayer.getLength();
178 | WritableMap map = Arguments.createMap();
179 | map.putBoolean("isPlaying", isPlaying);
180 | map.putDouble("position", position);
181 | map.putDouble("currentTime", currentTime);
182 | map.putDouble("duration", totalLength);
183 | updateVideoInfo();
184 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_PROGRESS);
185 | }
186 |
187 | mProgressUpdateHandler.postDelayed(mProgressUpdateRunnable, Math.round(mProgressUpdateInterval));
188 | }
189 | };
190 |
191 | mProgressUpdateHandler.postDelayed(mProgressUpdateRunnable, 0);
192 | }
193 | }.start();
194 | }
195 | }
196 |
197 |
198 | /*************
199 | * Events Listener
200 | *************/
201 |
202 | private View.OnLayoutChangeListener onLayoutChangeListener = new View.OnLayoutChangeListener() {
203 |
204 | @Override
205 | public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
206 | if (view.getWidth() > 0 && view.getHeight() > 0) {
207 | mVideoWidth = view.getWidth(); // 获取宽度
208 | mVideoHeight = view.getHeight(); // 获取高度
209 | if (mMediaPlayer != null) {
210 | IVLCVout vlcOut = mMediaPlayer.getVLCVout();
211 | vlcOut.setWindowSize(mVideoWidth, mVideoHeight);
212 | if (autoAspectRatio) {
213 | mMediaPlayer.setAspectRatio(mVideoWidth + ":" + mVideoHeight);
214 | }
215 | }
216 | }
217 | }
218 | };
219 |
220 | /**
221 | * 播放过程中的时间事件监听
222 | */
223 | private MediaPlayer.EventListener mPlayerListener = new MediaPlayer.EventListener() {
224 | long currentTime = 0;
225 | long totalLength = 0;
226 |
227 | @Override
228 | public void onEvent(MediaPlayer.Event event) {
229 | boolean isPlaying = mMediaPlayer.isPlaying();
230 | currentTime = mMediaPlayer.getTime();
231 | float position = mMediaPlayer.getPosition();
232 | totalLength = mMediaPlayer.getLength();
233 | WritableMap map = Arguments.createMap();
234 | map.putBoolean("isPlaying", isPlaying);
235 | map.putDouble("position", position);
236 | map.putDouble("currentTime", currentTime);
237 | map.putDouble("duration", totalLength);
238 |
239 | switch (event.type) {
240 | case MediaPlayer.Event.EndReached:
241 | map.putString("type", "Ended");
242 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_END);
243 | break;
244 | case MediaPlayer.Event.Playing:
245 | map.putString("type", "Playing");
246 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_IS_PLAYING);
247 | break;
248 | case MediaPlayer.Event.Opening:
249 | map.putString("type", "Opening");
250 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_OPEN);
251 | break;
252 | case MediaPlayer.Event.Paused:
253 | map.putString("type", "Paused");
254 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_PAUSED);
255 | break;
256 | case MediaPlayer.Event.Buffering:
257 |
258 | map.putDouble("bufferRate", event.getBuffering());
259 | map.putString("type", "Buffering");
260 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_VIDEO_BUFFERING);
261 | break;
262 | case MediaPlayer.Event.Stopped:
263 | map.putString("type", "Stopped");
264 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_VIDEO_STOPPED);
265 | break;
266 | case MediaPlayer.Event.EncounteredError:
267 | map.putString("type", "Error");
268 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_ON_ERROR);
269 |
270 | break;
271 | case MediaPlayer.Event.TimeChanged:
272 | map.putString("type", "TimeChanged");
273 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_SEEK);
274 | break;
275 | case MediaPlayer.Event.RecordChanged:
276 | map.putString("type", "RecordingPath");
277 | map.putBoolean("isRecording", event.getRecording());
278 | // Record started emits and event with the record path (but no file).
279 | // Only want to emit when recording has stopped and the recording is created.
280 | if(!event.getRecording() && event.getRecordPath() != null) {
281 | map.putString("recordPath", event.getRecordPath());
282 | }
283 | eventEmitter.sendEvent(map, VideoEventEmitter.EVENT_RECORDING_STATE);
284 | break;
285 | default:
286 | map.putString("type", event.type + "");
287 | eventEmitter.onVideoStateChange(map);
288 | break;
289 | }
290 |
291 | }
292 | };
293 |
294 | private IVLCVout.OnNewVideoLayoutListener onNewVideoLayoutListener = new IVLCVout.OnNewVideoLayoutListener() {
295 | @Override
296 | public void onNewVideoLayout(IVLCVout vout, int width, int height, int visibleWidth, int visibleHeight, int sarNum, int sarDen) {
297 | if (width * height == 0)
298 | return;
299 | // store video size
300 | mVideoWidth = width;
301 | mVideoHeight = height;
302 | mVideoVisibleWidth = visibleWidth;
303 | mVideoVisibleHeight = visibleHeight;
304 | mSarNum = sarNum;
305 | mSarDen = sarDen;
306 | WritableMap map = Arguments.createMap();
307 | map.putInt("mVideoWidth", mVideoWidth);
308 | map.putInt("mVideoHeight", mVideoHeight);
309 | map.putInt("mVideoVisibleWidth", mVideoVisibleWidth);
310 | map.putInt("mVideoVisibleHeight", mVideoVisibleHeight);
311 | map.putInt("mSarNum", mSarNum);
312 | map.putInt("mSarDen", mSarDen);
313 | map.putString("type", "onNewVideoLayout");
314 | eventEmitter.onVideoStateChange(map);
315 | }
316 | };
317 |
318 | IVLCVout.Callback callback = new IVLCVout.Callback() {
319 | @Override
320 | public void onSurfacesCreated(IVLCVout ivlcVout) {
321 | isSurfaceViewDestory = false;
322 | }
323 |
324 | @Override
325 | public void onSurfacesDestroyed(IVLCVout ivlcVout) {
326 | isSurfaceViewDestory = true;
327 | }
328 |
329 | };
330 |
331 |
332 | /*************
333 | * MediaPlayer
334 | *************/
335 |
336 |
337 | private void stopPlayback() {
338 | onStopPlayback();
339 | releasePlayer();
340 | }
341 |
342 | private void onStopPlayback() {
343 | setKeepScreenOn(false);
344 | audioManager.abandonAudioFocus(this);
345 | }
346 |
347 | private void createPlayer(boolean autoplayResume, boolean isResume) {
348 | releasePlayer();
349 | if (this.getSurfaceTexture() == null) {
350 | return;
351 | }
352 | try {
353 | final ArrayList cOptions = new ArrayList<>();
354 | String uriString = srcMap.hasKey("uri") ? srcMap.getString("uri") : null;
355 | //String extension = srcMap.hasKey("type") ? srcMap.getString("type") : null;
356 | boolean isNetwork = srcMap.hasKey("isNetwork") ? srcMap.getBoolean("isNetwork") : false;
357 | boolean autoplay = srcMap.hasKey("autoplay") ? srcMap.getBoolean("autoplay") : true;
358 | int initType = srcMap.hasKey("initType") ? srcMap.getInt("initType") : 1;
359 | ReadableArray mediaOptions = srcMap.hasKey("mediaOptions") ? srcMap.getArray("mediaOptions") : null;
360 | ReadableArray initOptions = srcMap.hasKey("initOptions") ? srcMap.getArray("initOptions") : null;
361 | Integer hwDecoderEnabled = srcMap.hasKey("hwDecoderEnabled") ? srcMap.getInt("hwDecoderEnabled") : null;
362 | Integer hwDecoderForced = srcMap.hasKey("hwDecoderForced") ? srcMap.getInt("hwDecoderForced") : null;
363 |
364 | if (initOptions != null) {
365 | ArrayList options = initOptions.toArrayList();
366 | for (int i = 0; i < options.size() - 1; i++) {
367 | String option = (String) options.get(i);
368 | cOptions.add(option);
369 | }
370 | }
371 | // Create LibVLC
372 | if (initType == 1) {
373 | libvlc = new LibVLC(getContext());
374 | } else {
375 | libvlc = new LibVLC(getContext(), cOptions);
376 | }
377 | // Create media player
378 | mMediaPlayer = new MediaPlayer(libvlc);
379 | setMutedModifier(mMuted);
380 | mMediaPlayer.setEventListener(mPlayerListener);
381 |
382 | // Register dialog callbacks for certificate handling
383 | Dialog.setCallbacks(libvlc, new Dialog.Callbacks() {
384 | @Override
385 | public void onDisplay(Dialog.QuestionDialog dialog) {
386 | handleCertificateDialog(dialog);
387 | }
388 |
389 | @Override
390 | public void onDisplay(Dialog.ErrorMessage dialog) {
391 | // Handle error dialogs if needed
392 | }
393 |
394 | @Override
395 | public void onDisplay(Dialog.LoginDialog dialog) {
396 | // Handle login dialogs if needed
397 | }
398 |
399 | @Override
400 | public void onDisplay(Dialog.ProgressDialog dialog) {
401 | // Handle progress dialogs if needed
402 | }
403 |
404 | @Override
405 | public void onCanceled(Dialog dialog) {
406 | // Handle dialog cancellation
407 | }
408 |
409 | @Override
410 | public void onProgressUpdate(Dialog.ProgressDialog dialog) {
411 | // Handle progress updates
412 | }
413 | });
414 | //this.getHolder().setKeepScreenOn(true);
415 | IVLCVout vlcOut = mMediaPlayer.getVLCVout();
416 | if (mVideoWidth > 0 && mVideoHeight > 0) {
417 | vlcOut.setWindowSize(mVideoWidth, mVideoHeight);
418 | if (autoAspectRatio) {
419 | mMediaPlayer.setAspectRatio(mVideoWidth + ":" + mVideoHeight);
420 | }
421 | //mMediaPlayer.setAspectRatio(mVideoWidth+":"+mVideoHeight);
422 | }
423 | DisplayMetrics dm = getResources().getDisplayMetrics();
424 | Media m = null;
425 | if (isNetwork) {
426 | Uri uri = Uri.parse(uriString);
427 | m = new Media(libvlc, uri);
428 | } else {
429 | m = new Media(libvlc, uriString);
430 | }
431 | m.setEventListener(mMediaListener);
432 | if (hwDecoderEnabled != null && hwDecoderForced != null) {
433 | boolean hmEnabled = false;
434 | boolean hmForced = false;
435 | if (hwDecoderEnabled >= 1) {
436 | hmEnabled = true;
437 | }
438 | if (hwDecoderForced >= 1) {
439 | hmForced = true;
440 | }
441 | m.setHWDecoderEnabled(hmEnabled, hmForced);
442 | }
443 | //添加media option
444 | if (mediaOptions != null) {
445 | ArrayList options = mediaOptions.toArrayList();
446 | for (int i = 0; i < options.size() - 1; i++) {
447 | String option = (String) options.get(i);
448 | m.addOption(option);
449 | }
450 | }
451 | mVideoInfo = null;
452 | mVideoInfoHash = null;
453 | mMediaPlayer.setMedia(m);
454 | m.release();
455 | mMediaPlayer.setScale(0);
456 | if (_subtitleUri != null) {
457 | mMediaPlayer.addSlave(Media.Slave.Type.Subtitle, _subtitleUri, true);
458 | }
459 |
460 | if (!vlcOut.areViewsAttached()) {
461 | vlcOut.addCallback(callback);
462 | // vlcOut.setVideoSurface(this.getSurfaceTexture());
463 | //vlcOut.setVideoSurface(this.getHolder().getSurface(), this.getHolder());
464 | //vlcOut.attachViews(onNewVideoLayoutListener);
465 | vlcOut.setVideoSurface(this.getSurfaceTexture());
466 | vlcOut.attachViews(onNewVideoLayoutListener);
467 | // vlcOut.attachSurfaceSlave(surfaceVideo,null,onNewVideoLayoutListener);
468 | //vlcOut.setVideoView(this);
469 | //vlcOut.attachViews(onNewVideoLayoutListener);
470 | }
471 | if (isResume) {
472 | if (autoplayResume) {
473 | mMediaPlayer.play();
474 | }
475 | } else {
476 | if (autoplay) {
477 | isPaused = false;
478 | mMediaPlayer.play();
479 | }
480 | }
481 | eventEmitter.loadStart();
482 |
483 | setProgressUpdateRunnable();
484 | } catch (Exception e) {
485 | e.printStackTrace();
486 | //Toast.makeText(getContext(), "Error creating player!", Toast.LENGTH_LONG).show();
487 | }
488 | }
489 |
490 | private void releasePlayer() {
491 | if (libvlc == null)
492 | return;
493 |
494 | final IVLCVout vout = mMediaPlayer.getVLCVout();
495 | vout.removeCallback(callback);
496 | vout.detachViews();
497 | //surfaceView.removeOnLayoutChangeListener(onLayoutChangeListener);
498 | mMediaPlayer.release();
499 | libvlc.release();
500 | libvlc = null;
501 |
502 | if(mProgressUpdateRunnable != null){
503 | mProgressUpdateHandler.removeCallbacks(mProgressUpdateRunnable);
504 | }
505 | }
506 |
507 | /**
508 | * 视频进度调整
509 | *
510 | * @param position
511 | */
512 | public void setPosition(float position) {
513 | if (mMediaPlayer != null) {
514 | if (position >= 0 && position <= 1) {
515 | mMediaPlayer.setPosition(position);
516 | }
517 | }
518 | }
519 |
520 | public void setSubtitleUri(String subtitleUri) {
521 | _subtitleUri = subtitleUri;
522 | if (mMediaPlayer != null) {
523 | mMediaPlayer.addSlave(Media.Slave.Type.Subtitle, _subtitleUri, true);
524 | }
525 | }
526 |
527 | /**
528 | * 设置资源路径
529 | *
530 | * @param uri
531 | * @param isNetStr
532 | */
533 | public void setSrc(String uri, boolean isNetStr, boolean autoplay) {
534 | this.src = uri;
535 | this.netStrTag = isNetStr;
536 | createPlayer(autoplay, false);
537 | }
538 |
539 | public void setSrc(ReadableMap src) {
540 | this.srcMap = src;
541 | createPlayer(true, false);
542 | }
543 |
544 | /**
545 | * 改变播放速率
546 | *
547 | * @param rateModifier
548 | */
549 | public void setRateModifier(float rateModifier) {
550 | if (mMediaPlayer != null) {
551 | mMediaPlayer.setRate(rateModifier);
552 | }
553 | }
554 |
555 | public void setmProgressUpdateInterval(float interval) {
556 | mProgressUpdateInterval = interval;
557 | createPlayer(true, false);
558 | }
559 |
560 |
561 | /**
562 | * 改变声音大小
563 | *
564 | * @param volumeModifier
565 | */
566 | public void setVolumeModifier(int volumeModifier) {
567 | if (mMediaPlayer != null) {
568 | mMediaPlayer.setVolume(volumeModifier);
569 | }
570 | }
571 |
572 | /**
573 | * 改变静音状态
574 | *
575 | * @param muted
576 | */
577 | public void setMutedModifier(boolean muted) {
578 | mMuted = muted;
579 | if (mMediaPlayer != null) {
580 | if (muted) {
581 | this.preVolume = mMediaPlayer.getVolume();
582 | mMediaPlayer.setVolume(0);
583 | } else {
584 | mMediaPlayer.setVolume(this.preVolume);
585 | }
586 | }
587 | }
588 |
589 | /**
590 | * 改变播放状态
591 | *
592 | * @param paused
593 | */
594 | public void setPausedModifier(boolean paused) {
595 | Log.i("paused:", "" + paused + ":" + mMediaPlayer);
596 | if (mMediaPlayer != null) {
597 | if (paused) {
598 | isPaused = true;
599 | mMediaPlayer.pause();
600 | } else {
601 | isPaused = false;
602 | mMediaPlayer.play();
603 | Log.i("do play:", true + "");
604 | }
605 | } else {
606 | createPlayer(!paused, false);
607 | }
608 | }
609 |
610 |
611 | /**
612 | * Take a screenshot of the current video frame
613 | *
614 | * @param path The file path where to save the screenshot
615 | * @return boolean indicating if the screenshot was taken successfully
616 | */
617 | public boolean doSnapshot(String path) {
618 | if (mMediaPlayer != null) {
619 | try {
620 | Bitmap bitmap = getBitmap();
621 | if (bitmap == null) {
622 | WritableMap event = Arguments.createMap();
623 | event.putBoolean("success", false);
624 | event.putString("error", "Failed to capture bitmap");
625 | eventEmitter.sendEvent(event, VideoEventEmitter.EVENT_ON_SNAPSHOT);
626 | return false;
627 | }
628 |
629 | File file = new File(path);
630 | file.getParentFile().mkdirs();
631 |
632 | FileOutputStream out = new FileOutputStream(file);
633 |
634 | String extension = path.substring(path.lastIndexOf(".") + 1);
635 | if (extension.equals("png")) {
636 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
637 | } else {
638 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
639 | }
640 | out.flush();
641 | out.close();
642 |
643 | bitmap.recycle();
644 |
645 | WritableMap event = Arguments.createMap();
646 | event.putBoolean("success", true);
647 | event.putString("path", path);
648 | eventEmitter.sendEvent(event, VideoEventEmitter.EVENT_ON_SNAPSHOT);
649 | return true;
650 | } catch (Exception e) {
651 | WritableMap event = Arguments.createMap();
652 | event.putBoolean("success", false);
653 | event.putString("error", e.getMessage());
654 | eventEmitter.sendEvent(event, VideoEventEmitter.EVENT_ON_SNAPSHOT);
655 | e.printStackTrace();
656 | return false;
657 | }
658 | }
659 | WritableMap event = Arguments.createMap();
660 | event.putBoolean("success", false);
661 | event.putString("error", "MediaPlayer is null");
662 | eventEmitter.sendEvent(event, VideoEventEmitter.EVENT_ON_SNAPSHOT);
663 | return false;
664 | }
665 |
666 |
667 | /**
668 | * 重新加载视频
669 | *
670 | * @param autoplay
671 | */
672 | public void doResume(boolean autoplay) {
673 | createPlayer(autoplay, true);
674 | }
675 |
676 |
677 | public void setRepeatModifier(boolean repeat) {
678 | }
679 |
680 |
681 | /**
682 | * 改变宽高比
683 | *
684 | * @param aspectRatio
685 | */
686 | public void setAspectRatio(String aspectRatio) {
687 | if (!autoAspectRatio && mMediaPlayer != null) {
688 | mMediaPlayer.setAspectRatio(aspectRatio);
689 | }
690 | }
691 |
692 | public void setAutoAspectRatio(boolean auto) {
693 | autoAspectRatio = auto;
694 | }
695 |
696 | public void setAudioTrack(int track) {
697 | if (mMediaPlayer != null) {
698 | mMediaPlayer.setAudioTrack(track);
699 | }
700 | }
701 |
702 | public void setTextTrack(int track) {
703 | if (mMediaPlayer != null) {
704 | mMediaPlayer.setSpuTrack(track);
705 | }
706 | }
707 |
708 | public void startRecording(String recordingPath) {
709 | if(mMediaPlayer == null) return;
710 | if(recordingPath != null) {
711 | mMediaPlayer.record(recordingPath);
712 | }
713 | }
714 |
715 | public void stopRecording() {
716 | if(mMediaPlayer == null) return;
717 | mMediaPlayer.record(null);
718 | }
719 |
720 | public void stopPlayer() {
721 | if(mMediaPlayer == null) return;
722 | mMediaPlayer.stop();
723 | }
724 |
725 | private void handleCertificateDialog(Dialog.QuestionDialog dialog) {
726 | String title = dialog.getTitle();
727 | String text = dialog.getText();
728 |
729 | Log.i(TAG, "Certificate dialog - Title: " + title + ", Text: " + text);
730 |
731 | // Check if it's a certificate validation dialog
732 | if (text != null && (text.contains("certificate") || text.contains("SSL") || text.contains("TLS") || text.contains("cert"))) {
733 | if (acceptInvalidCertificates) {
734 | // Auto-accept invalid certificate
735 | dialog.postAction(1); // Action 1 typically means "Accept"
736 | Log.i(TAG, "Auto-accepted certificate dialog");
737 | } else {
738 | // Reject invalid certificate (default secure behavior)
739 | dialog.postAction(2); // Action 2 typically means "Reject"
740 | Log.i(TAG, "Rejected certificate dialog (acceptInvalidCertificates=false)");
741 | }
742 | } else {
743 | // For non-certificate dialogs, dismiss
744 | dialog.dismiss();
745 | Log.i(TAG, "Dismissed non-certificate dialog");
746 | }
747 | }
748 |
749 | public void setAcceptInvalidCertificates(boolean accept) {
750 | this.acceptInvalidCertificates = accept;
751 | Log.i(TAG, "Set acceptInvalidCertificates to: " + accept);
752 | }
753 |
754 | public void cleanUpResources() {
755 | if (surfaceView != null) {
756 | surfaceView.removeOnLayoutChangeListener(onLayoutChangeListener);
757 | }
758 | stopPlayback();
759 | }
760 |
761 | @Override
762 | public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
763 | mVideoWidth = width;
764 | mVideoHeight = height;
765 | surfaceVideo = new Surface(surface);
766 | createPlayer(true, false);
767 | }
768 |
769 | @Override
770 | public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
771 |
772 | }
773 |
774 | @Override
775 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
776 | return true;
777 | }
778 |
779 | @Override
780 | public void onSurfaceTextureUpdated(SurfaceTexture surface) {
781 | // Log.i("onSurfaceTextureUpdated", "onSurfaceTextureUpdated");
782 | }
783 |
784 | private final Media.EventListener mMediaListener = new Media.EventListener() {
785 | @Override
786 | public void onEvent(Media.Event event) {
787 | switch (event.type) {
788 | case Media.Event.MetaChanged:
789 | Log.i(tag, "Media.Event.MetaChanged: =" + event.getMetaId());
790 | break;
791 | case Media.Event.ParsedChanged:
792 | Log.i(tag, "Media.Event.ParsedChanged =" + event.getMetaId());
793 |
794 | break;
795 | case Media.Event.StateChanged:
796 | Log.i(tag, "StateChanged =" + event.getMetaId());
797 | break;
798 | default:
799 | Log.i(tag, "Media.Event.type=" + event.type + " eventgetParsedStatus=" + event.getParsedStatus());
800 | break;
801 |
802 | }
803 | }
804 | };
805 |
806 | private void updateVideoInfo() {
807 | // Create a hash of the video info to compare for changes
808 | StringBuilder infoHash = new StringBuilder();
809 |
810 | infoHash.append("duration:").append(mMediaPlayer.getLength()).append(";");
811 |
812 | if(mMediaPlayer.getAudioTracksCount() > 0) {
813 | MediaPlayer.TrackDescription[] audioTracks = mMediaPlayer.getAudioTracks();
814 | infoHash.append("audioTracks:");
815 | for (MediaPlayer.TrackDescription track : audioTracks) {
816 | infoHash.append(track.id).append(":").append(track.name).append(",");
817 | }
818 | infoHash.append(";");
819 | }
820 |
821 | if(mMediaPlayer.getSpuTracksCount() > 0) {
822 | MediaPlayer.TrackDescription[] spuTracks = mMediaPlayer.getSpuTracks();
823 | infoHash.append("textTracks:");
824 | for (MediaPlayer.TrackDescription track : spuTracks) {
825 | infoHash.append(track.id).append(":").append(track.name).append(",");
826 | }
827 | infoHash.append(";");
828 | }
829 |
830 | Media.VideoTrack video = mMediaPlayer.getCurrentVideoTrack();
831 | if(video != null) {
832 | infoHash.append("videoSize:").append(video.width).append("x").append(video.height).append(";");
833 | }
834 |
835 | String currentHash = infoHash.toString();
836 |
837 | // Only send update if info has changed
838 | if (mVideoInfoHash == null || !mVideoInfoHash.equals(currentHash)) {
839 | WritableMap info = Arguments.createMap();
840 |
841 | info.putDouble("duration", mMediaPlayer.getLength());
842 |
843 | if(mMediaPlayer.getAudioTracksCount() > 0) {
844 | MediaPlayer.TrackDescription[] audioTracks = mMediaPlayer.getAudioTracks();
845 | WritableArray tracks = new WritableNativeArray();
846 | for (MediaPlayer.TrackDescription track : audioTracks) {
847 | WritableMap trackMap = Arguments.createMap();
848 | trackMap.putInt("id", track.id);
849 | trackMap.putString("name", track.name);
850 | tracks.pushMap(trackMap);
851 | }
852 | info.putArray("audioTracks", tracks);
853 | }
854 |
855 | if(mMediaPlayer.getSpuTracksCount() > 0) {
856 | MediaPlayer.TrackDescription[] spuTracks = mMediaPlayer.getSpuTracks();
857 | WritableArray tracks = new WritableNativeArray();
858 | for (MediaPlayer.TrackDescription track : spuTracks) {
859 | WritableMap trackMap = Arguments.createMap();
860 | trackMap.putInt("id", track.id);
861 | trackMap.putString("name", track.name);
862 | tracks.pushMap(trackMap);
863 | }
864 | info.putArray("textTracks", tracks);
865 | }
866 |
867 | Media.VideoTrack video2 = mMediaPlayer.getCurrentVideoTrack();
868 | if(video2 != null) {
869 | WritableMap mapVideoSize = Arguments.createMap();
870 | mapVideoSize.putInt("width", video2.width);
871 | mapVideoSize.putInt("height", video2.height);
872 | info.putMap("videoSize", mapVideoSize);
873 | }
874 |
875 | eventEmitter.sendEvent(info, VideoEventEmitter.EVENT_ON_LOAD);
876 | mVideoInfo = info;
877 | mVideoInfoHash = currentHash;
878 | }
879 | }
880 |
881 | /*private void changeSurfaceSize(boolean message) {
882 |
883 | if (mMediaPlayer != null) {
884 | final IVLCVout vlcVout = mMediaPlayer.getVLCVout();
885 | vlcVout.setWindowSize(screenWidth, screenHeight);
886 | }
887 |
888 | double displayWidth = screenWidth, displayHeight = screenHeight;
889 |
890 | if (screenWidth < screenHeight) {
891 | displayWidth = screenHeight;
892 | displayHeight = screenWidth;
893 | }
894 |
895 | // sanity check
896 | if (displayWidth * displayHeight <= 1 || mVideoWidth * mVideoHeight <= 1) {
897 | return;
898 | }
899 |
900 | // compute the aspect ratio
901 | double aspectRatio, visibleWidth;
902 | if (mSarDen == mSarNum) {
903 | *//* No indication about the density, assuming 1:1 *//*
904 | visibleWidth = mVideoVisibleWidth;
905 | aspectRatio = (double) mVideoVisibleWidth / (double) mVideoVisibleHeight;
906 | } else {
907 | *//* Use the specified aspect ratio *//*
908 | visibleWidth = mVideoVisibleWidth * (double) mSarNum / mSarDen;
909 | aspectRatio = visibleWidth / mVideoVisibleHeight;
910 | }
911 |
912 | // compute the display aspect ratio
913 | double displayAspectRatio = displayWidth / displayHeight;
914 |
915 | counter ++;
916 |
917 | switch (mCurrentSize) {
918 | case SURFACE_BEST_FIT:
919 | if(counter > 2)
920 | Toast.makeText(getContext(), "Best Fit", Toast.LENGTH_SHORT).show();
921 | if (displayAspectRatio < aspectRatio)
922 | displayHeight = displayWidth / aspectRatio;
923 | else
924 | displayWidth = displayHeight * aspectRatio;
925 | break;
926 | case SURFACE_FIT_HORIZONTAL:
927 | Toast.makeText(getContext(), "Fit Horizontal", Toast.LENGTH_SHORT).show();
928 | displayHeight = displayWidth / aspectRatio;
929 | break;
930 | case SURFACE_FIT_VERTICAL:
931 | Toast.makeText(getContext(), "Fit Horizontal", Toast.LENGTH_SHORT).show();
932 | displayWidth = displayHeight * aspectRatio;
933 | break;
934 | case SURFACE_FILL:
935 | Toast.makeText(getContext(), "Fill", Toast.LENGTH_SHORT).show();
936 | break;
937 | case SURFACE_16_9:
938 | Toast.makeText(getContext(), "16:9", Toast.LENGTH_SHORT).show();
939 | aspectRatio = 16.0 / 9.0;
940 | if (displayAspectRatio < aspectRatio)
941 | displayHeight = displayWidth / aspectRatio;
942 | else
943 | displayWidth = displayHeight * aspectRatio;
944 | break;
945 | case SURFACE_4_3:
946 | Toast.makeText(getContext(), "4:3", Toast.LENGTH_SHORT).show();
947 | aspectRatio = 4.0 / 3.0;
948 | if (displayAspectRatio < aspectRatio)
949 | displayHeight = displayWidth / aspectRatio;
950 | else
951 | displayWidth = displayHeight * aspectRatio;
952 | break;
953 | case SURFACE_ORIGINAL:
954 | Toast.makeText(getContext(), "Original", Toast.LENGTH_SHORT).show();
955 | displayHeight = mVideoVisibleHeight;
956 | displayWidth = visibleWidth;
957 | break;
958 | }
959 |
960 | // set display size
961 | int finalWidth = (int) Math.ceil(displayWidth * mVideoWidth / mVideoVisibleWidth);
962 | int finalHeight = (int) Math.ceil(displayHeight * mVideoHeight / mVideoVisibleHeight);
963 |
964 | SurfaceHolder holder = this.getHolder();
965 | holder.setFixedSize(finalWidth, finalHeight);
966 |
967 | ViewGroup.LayoutParams lp = this.getLayoutParams();
968 | lp.width = finalWidth;
969 | lp.height = finalHeight;
970 | this.setLayoutParams(lp);
971 | this.invalidate();
972 | }*/
973 | }
974 |
--------------------------------------------------------------------------------