├── 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 | 8 | 9 | 10 | 11 | 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 | --------------------------------------------------------------------------------