Merge branch 'react-native-video:master' into master
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
examples/
|
@ -1,4 +1,3 @@
|
||||
{
|
||||
"extends": "airbnb",
|
||||
"parser": "babel-eslint"
|
||||
"extends": "@react-native-community",
|
||||
}
|
3
.github/ISSUE_TEMPLATE.md
vendored
@ -10,8 +10,7 @@ Describe what you wanted to happen
|
||||
### Platform
|
||||
Which player are you experiencing the problem on:
|
||||
* iOS
|
||||
* Android ExoPlayer
|
||||
* Android MediaPlayer
|
||||
* Android
|
||||
* Windows UWP
|
||||
* Windows WPF
|
||||
|
||||
|
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,20 +10,21 @@ assignees: ''
|
||||
# Bug
|
||||
|
||||
<!--
|
||||
Please provide a clear and concise description of what the bug is.
|
||||
Include screenshots if needed.
|
||||
Please test using the latest release of the library, as maybe said bug has been already fixed.
|
||||
If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc)
|
||||
Before opening a ticket
|
||||
* Ensure the issue has not been already reported
|
||||
* Please test using the latest release of the library, as maybe said bug has been already fixed.
|
||||
* Provide a clear and concise description of what the bug is.
|
||||
* If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc)
|
||||
* Include screenshots if needed.
|
||||
-->
|
||||
|
||||
## Platform
|
||||
<!--
|
||||
Platform where your bug is happening. If Android, report if using Android or Android Exoplayer
|
||||
Platform where your bug is happening.
|
||||
-->
|
||||
Which player are you experiencing the problem on:
|
||||
* iOS
|
||||
* Android ExoPlayer
|
||||
* Android MediaPlayer
|
||||
* Android
|
||||
* Windows UWP
|
||||
* Windows WPF
|
||||
|
||||
|
60
.github/stale.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 3
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: true
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions. If you are having a similar problem, please open a
|
||||
new issue and reference this one instead of commenting on a stale or closed
|
||||
issue.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
unmarkComment: false
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: false
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 50
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
10
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: yarn --no-lockfile
|
||||
- run: yarn lint
|
8
.gitignore
vendored
@ -21,6 +21,7 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
Pods
|
||||
|
||||
# Android/IJ
|
||||
#
|
||||
@ -29,6 +30,9 @@ project.xcworkspace
|
||||
.gradle
|
||||
local.properties
|
||||
*.hprof
|
||||
.project
|
||||
.settings
|
||||
.classpath
|
||||
|
||||
# node.js
|
||||
#
|
||||
@ -46,3 +50,7 @@ buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
android/keystores/debug.keystore
|
||||
|
||||
# windows
|
||||
Deploy.binlog
|
||||
msbuild.binlog
|
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"requirePragma": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"jsxBracketSameLine": true,
|
||||
"parser": "flow"
|
||||
}
|
347
CHANGELOG.md
@ -1,139 +1,290 @@
|
||||
## Changelog
|
||||
|
||||
### Version 6.0.0-alpha.4
|
||||
|
||||
- ensure src is always provided to native player even if it is invalid [#2857](https://github.com/react-native-video/react-native-video/pull/2857)
|
||||
- Sample: Add react-native-video controls support [#2852](https://github.com/react-native-video/react-native-video/pull/2852)
|
||||
- Android: Switch Google's maven repository to default `google()` [#2860](https://github.com/react-native-video/react-native-video/pull/2860)
|
||||
- Android: Implement focusable prop so the video view can toggle whether it is focusable for non-touch devices [#2819](https://github.com/react-native-video/react-native-video/issues/2819)
|
||||
- Android: fix linter warning [#2891] (https://github.com/react-native-video/react-native-video/pull/2891)
|
||||
- Fix iOS RCTSwiftLog naming collision [#2868](https://github.com/react-native-video/react-native-video/issues/2868)
|
||||
- Added "homepage" to package.json [#2882](https://github.com/react-native-video/react-native-video/pull/2882)
|
||||
- Fix: memory leak issue on iOS [#2907](https://github.com/react-native-video/react-native-video/pull/2907)
|
||||
|
||||
### Version 6.0.0-alpha.3
|
||||
|
||||
- Fix ios build [#2854](https://github.com/react-native-video/react-native-video/pull/2854)
|
||||
|
||||
### Version 6.0.0-alpha.2
|
||||
|
||||
- Upgrade ExoPlayer to 2.18.1 [#2846](https://github.com/react-native-video/react-native-video/pull/2846)
|
||||
- Feature add new APIs to query supported features of device decoder (widevine level & codec capabilities) on android [#2740](https://github.com/react-native-video/react-native-video/pull/2740)
|
||||
- Feature add support of subtitle styling on android [#2759](https://github.com/react-native-video/react-native-video/pull/2759)
|
||||
- Fix Android #2690 ensure onEnd is not sent twice [#2690](https://github.com/react-native-video/react-native-video/issues/2690)
|
||||
- Fix Exoplayer progress not reported when paused [#2664](https://github.com/react-native-video/react-native-video/pull/2664)
|
||||
- Call playbackRateChange onPlay and onPause [#1493](https://github.com/react-native-video/react-native-video/pull/1493)
|
||||
- Fix being unable to disable sideloaded texttracks in the AVPlayer [#2679](https://github.com/react-native-video/react-native-video/pull/2679)
|
||||
- Fixed crash when iOS seek method called reject on the promise [#2743](https://github.com/react-native-video/react-native-video/pull/2743)
|
||||
- Fix maxBitRate property being ignored on Android [#2670](https://github.com/react-native-video/react-native-video/pull/2670)
|
||||
- Fix crash when the source is a cameraroll [#2639] (https://github.com/react-native-video/react-native-video/pull/2639)
|
||||
- Fix IOS UI frame drop on loading video [#2848] (https://github.com/react-native-video/react-native-video/pull/2848)
|
||||
|
||||
### Version 6.0.0-alpha.1
|
||||
|
||||
- Remove Android MediaPlayer support [#2724](https://github.com/react-native-video/react-native-video/pull/2724)
|
||||
**WARNING**: when switching from older version to V6, you need to remove all refrerences of android-exoplayer. This android-exoplayer folder has been renamed to android. Exoplayer is now the only player implementation supported.
|
||||
|
||||
- Replace Image.propTypes with ImagePropTypes. [#2718](https://github.com/react-native-video/react-native-video/pull/2718)
|
||||
- Fix iOS build caused by type mismatch [#2720](https://github.com/react-native-video/react-native-video/pull/2720)
|
||||
- ERROR TypeError: undefined is not an object (evaluating '_reactNative.Image.propTypes.resizeMode') [#2714](https://github.com/react-native-video/react-native-video/pull/2714)
|
||||
- Fix video endless loop when repeat set to false or not specified. [#2329](https://github.com/react-native-video/react-native-video/pull/2329)
|
||||
|
||||
### Version 6.0.0-alpha.0
|
||||
|
||||
- Support disabling buffering [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Fix AudioFocus bug that could cause the player to stop responding to play/pause in some instances. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Fix player crashing when it is being cleared. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Add support for customising back buffer duration and handle network errors gracefully to prevent releasing the player when network is lost. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Allow player to be init before source is provided, and later update once a source is provided. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Adds handling for providing a empty source in order to stop playback and clear out any existing content [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Add support for detecting if format is supported and exclude unsupported resolutions from auto quality selection and video track info in RN. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Improve error handling [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Add support for L1 to L3 Widevine fallback if playback fails initially. [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Reduce buffer size based on available heap [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Force garbage collection when there is no available memory [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Improve memory usage [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Support disabling screen recording [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Improved error capturing [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Fix DRM init crashes [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Improve progress reporting [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Fix progress loss when network connection is regained [#2689](https://github.com/react-native-video/react-native-video/pull/2689)
|
||||
- Add Google's maven repository to avoid build error [#2552](https://github.com/react-native-video/react-native-video/pull/2552)
|
||||
- Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633)
|
||||
- Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575)
|
||||
- Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181)
|
||||
- Disable pipController init if pictureInPicture is false [#2645](https://github.com/react-native-video/react-native-video/pull/2645)
|
||||
- Make sure modifiers are applied before playing [#2395](https://github.com/react-native-video/react-native-video/pull/2395)
|
||||
- Better support newer versions of RNW (64 and newer) [#2535](https://github.com/react-native-video/react-native-video/pull/2535)
|
||||
- Fix nil string uri parameter error [#695](https://github.com/react-native-video/react-native-video/pull/695)
|
||||
- (Breaking) Bump shaka-player to 3.3.2 [#2587](https://github.com/react-native-video/react-native-video/pull/2587)
|
||||
- Improve basic player example on android [#2662](https://github.com/react-native-video/react-native-video/pull/2662)
|
||||
- Ensure we always use `hideShutterView` before showing the `shutterView` on Android [#2609](https://github.com/react-native-video/react-native-video/pull/2609)
|
||||
- Convert iOS implementation to Swift [#2527](https://github.com/react-native-video/react-native-video/pull/2527)
|
||||
- Add iOS support for decoding offline sources [#2527](https://github.com/react-native-video/react-native-video/pull/2527)
|
||||
- Update basic example applications (React Native 0.63.4) [#2527](https://github.com/react-native-video/react-native-video/pull/2527)
|
||||
- Upgrade ExoPlayer to 2.17.1 [#2498](https://github.com/react-native-video/react-native-video/pull/2498)
|
||||
- Fix volume reset issue in exoPlayer [#2371](https://github.com/react-native-video/react-native-video/pull/2371)
|
||||
- Change WindowsTargetPlatformVersion to 10.0 [#2706](https://github.com/react-native-video/react-native-video/pull/2706)
|
||||
- Fixed Android seeking bug [#2712](https://github.com/react-native-video/react-native-video/pull/2712)
|
||||
- Fixed `onReadyForDisplay` not being called [#2721](https://github.com/react-native-video/react-native-video/pull/2721)
|
||||
- Fix type of `_eventDispatcher` on iOS target to match `bridge.eventDispatcher()` [#2720](https://github.com/react-native-video/react-native-video/pull/2720)
|
||||
|
||||
### Version 5.2.0
|
||||
|
||||
- Fix for tvOS native audio menu language selector
|
||||
- Update ExoPlayer to allow pre-init and content clear [#2412] (https://github.com/react-native-video/react-native-video/pull/2412)
|
||||
- iOS rate is reset to 1.0 after play/pause [#2167] (https://github.com/react-native-video/react-native-video/pull/2167)
|
||||
- Upgrade ExoPlayer to 2.13.2 [#2317] (https://github.com/react-native-video/react-native-video/pull/2317)
|
||||
- Fix AudioFocus pausing video when attempting to play [#2311] (https://github.com/react-native-video/react-native-video/pull/2311)
|
||||
|
||||
### Version 5.1.0-alpha9
|
||||
|
||||
- Add ARM64 support for windows [#2137](https://github.com/react-native-community/react-native-video/pull/2137)
|
||||
- Fix deprecated API bug for windows [#2119](https://github.com/react-native-video/react-native-video/pull/2119)
|
||||
- Added `rate` property and autolinking support for windows [#2206](https://github.com/react-native-video/react-native-video/pull/2206)
|
||||
|
||||
### Version 5.1.0-alpha8
|
||||
|
||||
- Fixing ID3 Frame Error When Receiving EventMessage in TimedMetadata [#2116](https://github.com/react-native-community/react-native-video/pull/2116)
|
||||
|
||||
### Version 5.1.0-alpha7
|
||||
|
||||
- Basic support for DRM on iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445)
|
||||
|
||||
### Version 5.1.0-alpha6
|
||||
|
||||
- Fix iOS bug which would break size of views when video is displayed with controls on a non full-screen React view. [#1931](https://github.com/react-native-community/react-native-video/pull/1931)
|
||||
- Fix video dimensions being undefined when playing HLS in ios. [#1992](https://github.com/react-native-community/react-native-video/pull/1992)
|
||||
- Add support for audio mix with other apps for iOS. [#1978](https://github.com/react-native-community/react-native-video/pull/1978)
|
||||
- Properly implement pending seek for iOS. [#1994](https://github.com/react-native-community/react-native-video/pull/1994)
|
||||
- Added `preferredForwardBufferDuration` (iOS) - the duration the player should buffer media from the network ahead of the playhead to guard against playback disruption. (#1944)
|
||||
- Added `currentPlaybackTime` (Android ExoPlayer, iOS) - when playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured, then this property will contain the epoch value in msec. (#1944)
|
||||
- Added `trackId` (Android ExoPlayer) - Configure an identifier for the video stream to link the playback context to the events emitted. (#1944)
|
||||
- Added preventsDisplaySleepDuringVideoPlayback (#2019)
|
||||
- Reverted the JS fullscreening for Android. [#2013](https://github.com/react-native-community/react-native-video/pull/2013)
|
||||
- Set iOS request headers without needing to edit RCTVideo.m. [#2014](https://github.com/react-native-community/react-native-video/pull/2014)
|
||||
- Fix exoplayer aspect ratio update on source changes [#2053](https://github.com/react-native-community/react-native-video/pull/2053)
|
||||
|
||||
### Version 5.1.0-alpha5
|
||||
|
||||
- Add support for react-native Windows Cpp/WinRT [#1893]((https://github.com/react-native-community/react-native-video/pull/1893))
|
||||
|
||||
### Version 5.1.0-alpha4
|
||||
|
||||
- Fix android play/pause bug related to full-screen mode [#1916](https://github.com/react-native-community/react-native-video/pull/1916)
|
||||
|
||||
### Version 5.1.0-alpha3
|
||||
|
||||
- Improve Android Audio Focus [#1897](https://github.com/react-native-community/react-native-video/pull/1897)
|
||||
|
||||
### Version 5.1.0-alpha2
|
||||
|
||||
- Added support for full-screen functionality in Android Exoplayer [#1730](https://github.com/react-native-community/react-native-video/pull/1730)
|
||||
|
||||
### Version 5.1.0-alpha1
|
||||
* Fixed Exoplayer doesn't work with mute=true (Android). [#1696](https://github.com/react-native-community/react-native-video/pull/1696)
|
||||
* Added support for automaticallyWaitsToMinimizeStalling property (iOS) [#1723](https://github.com/react-native-community/react-native-video/pull/1723)
|
||||
* Bump Exoplayer to 2.10.4, remove deprecated usages of Exoplayer methods (Android). [#1753](https://github.com/react-native-community/react-native-video/pull/1753)
|
||||
* Preserve Exoplayer BandwidthMeter instance across video plays, this should noticeably improve streaming bandwidth detection (Android).
|
||||
|
||||
- Fixed Exoplayer doesn't work with mute=true (Android). [#1696](https://github.com/react-native-community/react-native-video/pull/1696)
|
||||
- Added support for automaticallyWaitsToMinimizeStalling property (iOS) [#1723](https://github.com/react-native-community/react-native-video/pull/1723)
|
||||
- Bump Exoplayer to 2.10.4, remove deprecated usages of Exoplayer methods (Android). [#1753](https://github.com/react-native-community/react-native-video/pull/1753)
|
||||
- Preserve Exoplayer BandwidthMeter instance across video plays, this should noticeably improve streaming bandwidth detection (Android).
|
||||
|
||||
### Version 5.0.2
|
||||
* Fix crash when RCTVideo's superclass doesn't observe the keyPath 'frame' (iOS) [#1720](https://github.com/react-native-community/react-native-video/pull/1720)
|
||||
|
||||
- Fix crash when RCTVideo's superclass doesn't observe the keyPath 'frame' (iOS) [#1720](https://github.com/react-native-community/react-native-video/pull/1720)
|
||||
|
||||
### Version 5.0.1
|
||||
* Fix AndroidX Support bad merge
|
||||
|
||||
- Fix AndroidX Support bad merge
|
||||
|
||||
### Version 5.0.0 [Deprecated]
|
||||
* AndroidX Support
|
||||
|
||||
- AndroidX Support
|
||||
|
||||
### Version 4.4.4
|
||||
* Handle racing conditions when props are setted on exoplayer
|
||||
|
||||
- Handle racing conditions when props are settled on Exoplayer
|
||||
|
||||
### Version 4.4.3
|
||||
* Fix mute/unmute when controls are present (iOS) [#1654](https://github.com/react-native-community/react-native-video/pull/1654)
|
||||
* Fix Android videos being able to play with background music/audio from other apps.
|
||||
* Fixed memory leak on iOS when using `controls` [#1647](https://github.com/react-native-community/react-native-video/pull/1647)
|
||||
* (Android) Update gradle and target SDK [#1629](https://github.com/react-native-community/react-native-video/pull/1629)
|
||||
* Fix iOS stressed mount/unmount crash [#1646](https://github.com/react-native-community/react-native-video/pull/1646)
|
||||
|
||||
- Fix mute/unmute when controls are present (iOS) [#1654](https://github.com/react-native-community/react-native-video/pull/1654)
|
||||
- Fix Android videos being able to play with background music/audio from other apps.
|
||||
- Fixed memory leak on iOS when using `controls` [#1647](https://github.com/react-native-community/react-native-video/pull/1647)
|
||||
- (Android) Update gradle and target SDK [#1629](https://github.com/react-native-community/react-native-video/pull/1629)
|
||||
- Fix iOS stressed mount/unmount crash [#1646](https://github.com/react-native-community/react-native-video/pull/1646)
|
||||
|
||||
### Version 4.4.2
|
||||
* Change compileOnly to implementation on gradle (for newer gradle versions and react-native 0.59 support) [#1592](https://github.com/react-native-community/react-native-video/pull/1592)
|
||||
* Replaced RCTBubblingEventBlock events by RCTDirectEventBlock to avoid event name collisions [#1625](https://github.com/react-native-community/react-native-video/pull/1625)
|
||||
* Added `onPlaybackRateChange` to README [#1578](https://github.com/react-native-community/react-native-video/pull/1578)
|
||||
* Added `onReadyForDisplay` to README [#1627](https://github.com/react-native-community/react-native-video/pull/1627)
|
||||
* Improved handling of poster image. Fixes bug with displaying video and poster simultaneously. [#1627](https://github.com/react-native-community/react-native-video/pull/1627)
|
||||
* Fix background audio stopping on iOS when using `controls` [#1614](https://github.com/react-native-community/react-native-video/pull/1614)
|
||||
|
||||
- Change compileOnly to implementation on gradle (for newer gradle versions and react-native 0.59 support) [#1592](https://github.com/react-native-community/react-native-video/pull/1592)
|
||||
- Replaced RCTBubblingEventBlock events by RCTDirectEventBlock to avoid event name collisions [#1625](https://github.com/react-native-community/react-native-video/pull/1625)
|
||||
- Added `onPlaybackRateChange` to README [#1578](https://github.com/react-native-community/react-native-video/pull/1578)
|
||||
- Added `onReadyForDisplay` to README [#1627](https://github.com/react-native-community/react-native-video/pull/1627)
|
||||
- Improved handling of poster image. Fixes bug with displaying video and poster simultaneously. [#1627](https://github.com/react-native-community/react-native-video/pull/1627)
|
||||
- Fix background audio stopping on iOS when using `controls` [#1614](https://github.com/react-native-community/react-native-video/pull/1614)
|
||||
|
||||
### Version 4.4.1
|
||||
* Fix tvOS picture-in-picture compilation regression [#1518](https://github.com/react-native-community/react-native-video/pull/1518)
|
||||
* fullscreen rotation issues with iOS built-in controls [#1441](https://github.com/react-native-community/react-native-video/pull/1441)
|
||||
* Fix player freeze when playing audio files on ExoPlayer [#1529](https://github.com/react-native-community/react-native-video/pull/1529)
|
||||
|
||||
- Fix tvOS picture-in-picture compilation regression [#1518](https://github.com/react-native-community/react-native-video/pull/1518)
|
||||
- fullscreen rotation issues with iOS built-in controls [#1441](https://github.com/react-native-community/react-native-video/pull/1441)
|
||||
- Fix player freeze when playing audio files on ExoPlayer [#1529](https://github.com/react-native-community/react-native-video/pull/1529)
|
||||
|
||||
### Version 4.4.0
|
||||
* Fix runtime warning by replacing `UIManager.RCTVideo` with `UIManager.getViewManagerConfig('RCTVideo')` (and ensuring backwards compat) [#1487](https://github.com/react-native-community/react-native-video/pull/1487)
|
||||
* Fix loading package resolved videos when using video-caching [#1438](https://github.com/react-native-community/react-native-video/pull/1438)
|
||||
* Fix "message sent to deallocated instance" crash on ios [#1482](https://github.com/react-native-community/react-native-video/pull/1482)
|
||||
* Display a warning when source is empty [#1478](https://github.com/react-native-community/react-native-video/pull/1478)
|
||||
* Don't crash on iOS for an empty source [#1246](https://github.com/react-native-community/react-native-video/pull/1246)
|
||||
* Recover from from transient internet failures when loading on ExoPlayer [#1448](https://github.com/react-native-community/react-native-video/pull/1448)
|
||||
* Add controls support for ExoPlayer [#1414](https://github.com/react-native-community/react-native-video/pull/1414)
|
||||
* Fix check for text tracks when iOS caching enabled [#1387](https://github.com/react-native-community/react-native-video/pull/1387)
|
||||
* Add support for Picture in Picture on iOS [#1325](https://github.com/react-native-community/react-native-video/pull/1325)
|
||||
* Fix UIManager undefined variable [#1488](https://github.com/react-native-community/react-native-video/pull/1488)
|
||||
|
||||
- Fix runtime warning by replacing `UIManager.RCTVideo` with `UIManager.getViewManagerConfig('RCTVideo')` (and ensuring backwards compat) [#1487](https://github.com/react-native-community/react-native-video/pull/1487)
|
||||
- Fix loading package resolved videos when using video-caching [#1438](https://github.com/react-native-community/react-native-video/pull/1438)
|
||||
- Fix "message sent to deallocated instance" crash on ios [#1482](https://github.com/react-native-community/react-native-video/pull/1482)
|
||||
- Display a warning when source is empty [#1478](https://github.com/react-native-community/react-native-video/pull/1478)
|
||||
- Don't crash on iOS for an empty source [#1246](https://github.com/react-native-community/react-native-video/pull/1246)
|
||||
- Recover from from transient internet failures when loading on ExoPlayer [#1448](https://github.com/react-native-community/react-native-video/pull/1448)
|
||||
- Add controls support for ExoPlayer [#1414](https://github.com/react-native-community/react-native-video/pull/1414)
|
||||
- Fix check for text tracks when iOS caching enabled [#1387](https://github.com/react-native-community/react-native-video/pull/1387)
|
||||
- Add support for Picture in Picture on iOS [#1325](https://github.com/react-native-community/react-native-video/pull/1325)
|
||||
- Fix UIManager undefined variable [#1488](https://github.com/react-native-community/react-native-video/pull/1488)
|
||||
|
||||
### Version 4.3.0
|
||||
* Fix iOS video not displaying after switching source [#1395](https://github.com/react-native-community/react-native-video/pull/1395)
|
||||
* Add the filterEnabled flag, fixes iOS video start time regression [#1384](https://github.com/react-native-community/react-native-video/pull/1384)
|
||||
* Fix text not appearing in release builds of Android apps [#1373](https://github.com/react-native-community/react-native-video/pull/1373)
|
||||
* Update to ExoPlayer 2.9.3 [#1406](https://github.com/react-native-community/react-native-video/pull/1406)
|
||||
* Add video track selection & onBandwidthUpdate [#1199](https://github.com/react-native-community/react-native-video/pull/1199)
|
||||
* Recovery from transient internet failures and props to configure the custom retry count [#1448](https://github.com/react-native-community/react-native-video/pull/1448)
|
||||
|
||||
- Fix iOS video not displaying after switching source [#1395](https://github.com/react-native-community/react-native-video/pull/1395)
|
||||
- Add the filterEnabled flag, fixes iOS video start time regression [#1384](https://github.com/react-native-community/react-native-video/pull/1384)
|
||||
- Fix text not appearing in release builds of Android apps [#1373](https://github.com/react-native-community/react-native-video/pull/1373)
|
||||
- Update to ExoPlayer 2.9.3 [#1406](https://github.com/react-native-community/react-native-video/pull/1406)
|
||||
- Add video track selection & onBandwidthUpdate [#1199](https://github.com/react-native-community/react-native-video/pull/1199)
|
||||
- Recovery from transient internet failures and props to configure the custom retry count [#1448](https://github.com/react-native-community/react-native-video/pull/1448)
|
||||
|
||||
### Version 4.2.0
|
||||
* Don't initialize filters on iOS unless a filter is set. This was causing a startup performance regression [#1360](https://github.com/react-native-community/react-native-video/pull/1360)
|
||||
* Support setting the maxBitRate [#1310](https://github.com/react-native-community/react-native-video/pull/1310)
|
||||
* Fix useTextureView not defaulting to true [#1383](https://github.com/react-native-community/react-native-video/pull/1383)
|
||||
* Fix crash on MediaPlayer w/ Android 4.4 & avoid memory leak [#1328](https://github.com/react-native-community/react-native-video/pull/1328)
|
||||
|
||||
- Don't initialize filters on iOS unless a filter is set. This was causing a startup performance regression [#1360](https://github.com/react-native-community/react-native-video/pull/1360)
|
||||
- Support setting the maxBitRate [#1310](https://github.com/react-native-community/react-native-video/pull/1310)
|
||||
- Fix useTextureView not defaulting to true [#1383](https://github.com/react-native-community/react-native-video/pull/1383)
|
||||
- Fix crash on MediaPlayer w/ Android 4.4 & avoid memory leak [#1328](https://github.com/react-native-community/react-native-video/pull/1328)
|
||||
|
||||
### Version 4.1.0
|
||||
* Generate onSeek on Android ExoPlayer & MediaPlayer after seek completes [#1351](https://github.com/react-native-community/react-native-video/pull/1351)
|
||||
* Remove unneeded onVideoSaved event [#1350](https://github.com/react-native-community/react-native-video/pull/1350)
|
||||
* Disable AirPlay if sidecar text tracks are enabled [#1304](https://github.com/react-native-community/react-native-video/pull/1304)
|
||||
* Add possibility to remove black screen while video is loading in Exoplayer [#1355](https://github.com/react-native-community/react-native-video/pull/1355)
|
||||
|
||||
- Generate onSeek on Android ExoPlayer & MediaPlayer after seek completes [#1351](https://github.com/react-native-community/react-native-video/pull/1351)
|
||||
- Remove unneeded onVideoSaved event [#1350](https://github.com/react-native-community/react-native-video/pull/1350)
|
||||
- Disable AirPlay if sidecar text tracks are enabled [#1304](https://github.com/react-native-community/react-native-video/pull/1304)
|
||||
- Add possibility to remove black screen while video is loading in Exoplayer [#1355](https://github.com/react-native-community/react-native-video/pull/1355)
|
||||
|
||||
### Version 4.0.1
|
||||
* Add missing files to package.json [#1342](https://github.com/react-native-community/react-native-video/pull/1342)
|
||||
|
||||
- Add missing files to package.json [#1342](https://github.com/react-native-community/react-native-video/pull/1342)
|
||||
|
||||
### Version 4.0.0
|
||||
* Partial support for timed metadata on Android MediaPlayer [#707](https://github.com/react-native-community/react-native-video/pull/707)
|
||||
* Support video caching for iOS [#955](https://github.com/react-native-community/react-native-video/pull/955)
|
||||
* Video caching cleanups [#1172](https://github.com/react-native-community/react-native-video/pull/1172)
|
||||
* Add ipod-library support [#926](https://github.com/react-native-community/react-native-video/pull/926/files)
|
||||
* Fix crash on ExoPlayer when there are no audio tracks [#1233](https://github.com/react-native-community/react-native-video/pull/1233)
|
||||
* Reduce package size [#1231](https://github.com/react-native-community/react-native-video/pull/1231)
|
||||
* Remove unnecessary import in TextTrackType [#1229](https://github.com/react-native-community/react-native-video/pull/1229)
|
||||
* Prevent flash between poster and video [#1167](https://github.com/react-native-community/react-native-video/pull/1167)
|
||||
* Support react-native-dom [#1253](https://github.com/react-native-community/react-native-video/pull/1253)
|
||||
* Update to ExoPlayer 2.8.2. Android SDK 26 now required [#1170](https://github.com/react-native-community/react-native-video/pull/1170)
|
||||
* Update to ExoPlayer 2.8.4 [#1266](https://github.com/react-native-community/react-native-video/pull/1266)
|
||||
* Add fullscreenOrientation option for iOS [#1215](https://github.com/react-native-community/react-native-video/pull/1215)
|
||||
* Update to ExoPlayer 2.9.0 [#1285](https://github.com/react-native-community/react-native-video/pull/1285)
|
||||
* Switch useTextureView to default to `true` [#1286](https://github.com/react-native-community/react-native-video/pull/1286)
|
||||
* Re-add fullscreenAutorotate prop [#1303](https://github.com/react-native-community/react-native-video/pull/1303)
|
||||
* Make seek throw a useful error for NaN values [#1283](https://github.com/react-native-community/react-native-video/pull/1283)
|
||||
* Video Filters and Save Video [#1306](https://github.com/react-native-community/react-native-video/pull/1306)
|
||||
* Fix: volume should not change on onAudioFocusChange event [#1327](https://github.com/react-native-community/react-native-video/pull/1327)
|
||||
* Update ExoPlayer to 2.9.1 and OkHTTP to 3.12.0 [#1338](https://github.com/react-native-community/react-native-video/pull/1338)
|
||||
|
||||
- Partial support for timed metadata on Android MediaPlayer [#707](https://github.com/react-native-community/react-native-video/pull/707)
|
||||
- Support video caching for iOS [#955](https://github.com/react-native-community/react-native-video/pull/955)
|
||||
- Video caching cleanups [#1172](https://github.com/react-native-community/react-native-video/pull/1172)
|
||||
- Add ipod-library support [#926](https://github.com/react-native-community/react-native-video/pull/926/files)
|
||||
- Fix crash on ExoPlayer when there are no audio tracks [#1233](https://github.com/react-native-community/react-native-video/pull/1233)
|
||||
- Reduce package size [#1231](https://github.com/react-native-community/react-native-video/pull/1231)
|
||||
- Remove unnecessary import in TextTrackType [#1229](https://github.com/react-native-community/react-native-video/pull/1229)
|
||||
- Prevent flash between poster and video [#1167](https://github.com/react-native-community/react-native-video/pull/1167)
|
||||
- Support react-native-dom [#1253](https://github.com/react-native-community/react-native-video/pull/1253)
|
||||
- Update to ExoPlayer 2.8.2. Android SDK 26 now required [#1170](https://github.com/react-native-community/react-native-video/pull/1170)
|
||||
- Update to ExoPlayer 2.8.4 [#1266](https://github.com/react-native-community/react-native-video/pull/1266)
|
||||
- Add fullscreenOrientation option for iOS [#1215](https://github.com/react-native-community/react-native-video/pull/1215)
|
||||
- Update to ExoPlayer 2.9.0 [#1285](https://github.com/react-native-community/react-native-video/pull/1285)
|
||||
- Switch useTextureView to default to `true` [#1286](https://github.com/react-native-community/react-native-video/pull/1286)
|
||||
- Re-add fullscreenAutorotate prop [#1303](https://github.com/react-native-community/react-native-video/pull/1303)
|
||||
- Make seek throw a useful error for NaN values [#1283](https://github.com/react-native-community/react-native-video/pull/1283)
|
||||
- Video Filters and Save Video [#1306](https://github.com/react-native-community/react-native-video/pull/1306)
|
||||
- Fix: volume should not change on onAudioFocusChange event [#1327](https://github.com/react-native-community/react-native-video/pull/1327)
|
||||
- Update ExoPlayer to 2.9.1 and OkHTTP to 3.12.0 [#1338](https://github.com/react-native-community/react-native-video/pull/1338)
|
||||
|
||||
### Version 3.2.0
|
||||
* Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138)
|
||||
* Simplify default Android SDK code [#1145](https://github.com/react-native-community/react-native-video/pull/1145) [#1146](https://github.com/react-native-community/react-native-video/pull/1146)
|
||||
* Various iOS sideloaded text track fixes [#1157](https://github.com/react-native-community/react-native-video/pull/1157)
|
||||
* Fix #1150 where assets with bundled assets don't work on iOS in release mode [#1162](https://github.com/react-native-community/react-native-video/pull/1162)
|
||||
* Support configuring the buffer on Android ExoPlayer [#1160](https://github.com/react-native-community/react-native-video/pull/1160)
|
||||
* Prevent sleep from sleeping while videos are playing on Android MediaPlayer [#1117](https://github.com/react-native-community/react-native-video/pull/1117)
|
||||
* Update NewtonSoft JSON to match react-native-windows version [#1169](https://github.com/react-native-community/react-native-video/pull/1169)
|
||||
|
||||
- Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138)
|
||||
- Simplify default Android SDK code [#1145](https://github.com/react-native-community/react-native-video/pull/1145) [#1146](https://github.com/react-native-community/react-native-video/pull/1146)
|
||||
- Various iOS sideloaded text track fixes [#1157](https://github.com/react-native-community/react-native-video/pull/1157)
|
||||
- Fix #1150 where assets with bundled assets don't work on iOS in release mode [#1162](https://github.com/react-native-community/react-native-video/pull/1162)
|
||||
- Support configuring the buffer on Android ExoPlayer [#1160](https://github.com/react-native-community/react-native-video/pull/1160)
|
||||
- Prevent sleep from sleeping while videos are playing on Android MediaPlayer [#1117](https://github.com/react-native-community/react-native-video/pull/1117)
|
||||
- Update NewtonSoft JSON to match react-native-windows version [#1169](https://github.com/react-native-community/react-native-video/pull/1169)
|
||||
|
||||
### Version 3.1.0
|
||||
* Support sidecar text tracks on iOS [#1109](https://github.com/react-native-community/react-native-video/pull/1109)
|
||||
* Support onAudioBecomingNoisy on iOS [#1131](https://github.com/react-native-community/react-native-video/pull/1131)
|
||||
|
||||
- Support sidecar text tracks on iOS [#1109](https://github.com/react-native-community/react-native-video/pull/1109)
|
||||
- Support onAudioBecomingNoisy on iOS [#1131](https://github.com/react-native-community/react-native-video/pull/1131)
|
||||
|
||||
### Version 3.0
|
||||
* Inherit Android buildtools and SDK version from the root project [#1081](https://github.com/react-native-community/react-native-video/pull/1081)
|
||||
* Automatically play on ExoPlayer when the paused prop is not set [#1083](https://github.com/react-native-community/react-native-video/pull/1083)
|
||||
* Preserve Android MediaPlayer paused prop when backgrounding [#1082](https://github.com/react-native-community/react-native-video/pull/1082)
|
||||
* Support specifying headers on ExoPlayer as part of the source [#805](https://github.com/react-native-community/react-native-video/pull/805)
|
||||
* Prevent iOS onLoad event during seeking [#1088](https://github.com/react-native-community/react-native-video/pull/1088)
|
||||
* ExoPlayer playableDuration incorrect [#1089](https://github.com/react-native-community/react-native-video/pull/1089)
|
||||
|
||||
- Inherit Android buildtools and SDK version from the root project [#1081](https://github.com/react-native-community/react-native-video/pull/1081)
|
||||
- Automatically play on ExoPlayer when the paused prop is not set [#1083](https://github.com/react-native-community/react-native-video/pull/1083)
|
||||
- Preserve Android MediaPlayer paused prop when backgrounding [#1082](https://github.com/react-native-community/react-native-video/pull/1082)
|
||||
- Support specifying headers on ExoPlayer as part of the source [#805](https://github.com/react-native-community/react-native-video/pull/805)
|
||||
- Prevent iOS onLoad event during seeking [#1088](https://github.com/react-native-community/react-native-video/pull/1088)
|
||||
- ExoPlayer playableDuration incorrect [#1089](https://github.com/react-native-community/react-native-video/pull/1089)
|
||||
|
||||
### Version 2.3.1
|
||||
* Revert PR to inherit Android SDK versions from root project. Re-add in 3.0 [#1080](https://github.com/react-native-community/react-native-video/pull/1080)
|
||||
|
||||
- Revert PR to inherit Android SDK versions from root project. Re-add in 3.0 [#1080](https://github.com/react-native-community/react-native-video/pull/1080)
|
||||
|
||||
### Version 2.3.0
|
||||
* Support allowsExternalPlayback on iOS [#1057](https://github.com/react-native-community/react-native-video/pull/1057)
|
||||
* Inherit Android buildtools and SDK version from the root project [#999](https://github.com/react-native-community/react-native-video/pull/999)
|
||||
* Fix bug that caused ExoPlayer to start paused if playInBackground was set [#833](https://github.com/react-native-community/react-native-video/pull/833)
|
||||
* Fix crash if clearing an observer on iOS that was already cleared [#1075](https://github.com/react-native-community/react-native-video/pull/1075)
|
||||
* Add audioOnly prop for music files [#1039](https://github.com/react-native-community/react-native-video/pull/1039)
|
||||
* Support seeking with more exact tolerance on iOS [#1076](https://github.com/react-native-community/react-native-video/pull/1076)
|
||||
|
||||
- Support allowsExternalPlayback on iOS [#1057](https://github.com/react-native-community/react-native-video/pull/1057)
|
||||
- Inherit Android buildtools and SDK version from the root project [#999](https://github.com/react-native-community/react-native-video/pull/999)
|
||||
- Fix bug that caused ExoPlayer to start paused if playInBackground was set [#833](https://github.com/react-native-community/react-native-video/pull/833)
|
||||
- Fix crash if clearing an observer on iOS that was already cleared [#1075](https://github.com/react-native-community/react-native-video/pull/1075)
|
||||
- Add audioOnly prop for music files [#1039](https://github.com/react-native-community/react-native-video/pull/1039)
|
||||
- Support seeking with more exact tolerance on iOS [#1076](https://github.com/react-native-community/react-native-video/pull/1076)
|
||||
|
||||
### Version 2.2.0
|
||||
* Text track selection support for iOS & ExoPlayer [#1049](https://github.com/react-native-community/react-native-video/pull/1049)
|
||||
* Support outputting to a TextureView on Android ExoPlayer [#1058](https://github.com/react-native-community/react-native-video/pull/1058)
|
||||
* Support changing the left/right balance on Android MediaPlayer [#1051](https://github.com/react-native-community/react-native-video/pull/1051)
|
||||
* Prevent multiple onEnd notifications on iOS [#832](https://github.com/react-native-community/react-native-video/pull/832)
|
||||
* Fix doing a partial swipe on iOS causing a black screen [#1048](https://github.com/react-native-community/react-native-video/pull/1048)
|
||||
* Fix crash when switching to a new source on iOS [#974](https://github.com/react-native-community/react-native-video/pull/974)
|
||||
* Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
|
||||
* Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
|
||||
* Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
|
||||
* Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)
|
||||
|
||||
- Text track selection support for iOS & ExoPlayer [#1049](https://github.com/react-native-community/react-native-video/pull/1049)
|
||||
- Support outputting to a TextureView on Android ExoPlayer [#1058](https://github.com/react-native-community/react-native-video/pull/1058)
|
||||
- Support changing the left/right balance on Android MediaPlayer [#1051](https://github.com/react-native-community/react-native-video/pull/1051)
|
||||
- Prevent multiple onEnd notifications on iOS [#832](https://github.com/react-native-community/react-native-video/pull/832)
|
||||
- Fix doing a partial swipe on iOS causing a black screen [#1048](https://github.com/react-native-community/react-native-video/pull/1048)
|
||||
- Fix crash when switching to a new source on iOS [#974](https://github.com/react-native-community/react-native-video/pull/974)
|
||||
- Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
|
||||
- Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
|
||||
- Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
|
||||
- Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)
|
||||
|
31
CONTRIBUTING.md
Normal file
@ -0,0 +1,31 @@
|
||||
## Issues
|
||||
|
||||
* New issues are reviewed and if they require additional work will be marked with the [`triage needed`](https://github.com/react-native-video/react-native-video/labels/triage%20needed) label. This is an open call for help from the community to verify the issue and help categorize it. If an issue stays in this state for a long time, it will be closed unresolved.
|
||||
* Once an issue has been reviewed it will be labeled with [`help wanted`](https://github.com/react-native-video/react-native-video/labels/help%20wanted) to indicate it is ready to be worked on. Please wait for this label before submitting a PR to avoid spending time on something that is likely to be rejected.
|
||||
|
||||
## Cleanup
|
||||
|
||||
* Given the history of this project, we are going to be more aggressive than usual in keeping things clean. We are working with limited resources and do not want to return to the 1000+ open issues state. This is not meant to be disrespectful or hostile. It is just a way to keep the limited resources we have focused. If your issue was closed prematurely, just chime in and engage!
|
||||
* Issues and pull requests that become stale (60 days of inactivity) will be closed unless assigned and show progress.
|
||||
* If the issue creator fails to provide additional information within a week when asked, we may close the issue to keep things tidy (but you can always comment back and we can reopen).
|
||||
|
||||
## Pull Requests
|
||||
|
||||
* Please open an issue before opening a PR to make sure the proposed change is wanted and is likely to be merged. We don't want you to waste your time!
|
||||
* Pull requests require 1-3 approved reviews to be merged.
|
||||
* The number of reviews depends on the complexity by adding up (max of 3):
|
||||
* `1` reviewer for each PR
|
||||
* `1` if more than 3 files and/or 30 lines of code changed
|
||||
* `1` for each native platform code changes involved
|
||||
|
||||
For example, a single file JS code change requires 1 review while a 3 files iOS code change requires 3 reviews. As soon as the reviews show up as approved without any requested changes, the PR will be merged into the next milestone.
|
||||
|
||||
* Reviewers will be asked to assign a risk level when they are done from 1 (super safe) to 5 (super risky). A release with any risk level 4 or 5 will be published as a major version, otherwise as a patch or minor based on the changes. Prepare for some large version increments while we get more comfortable... (but remember versions are free).
|
||||
|
||||
* If you have time to help out, look for the [`review requested`](https://github.com/react-native-video/react-native-video/labels/review%20requested) label. It will have another numeric label with it (`1`, `2`, or `3` indicating how many more reviews are needed to merge).
|
||||
|
||||
## Releases
|
||||
|
||||
* Aim for a bi-weekly (every other week) release to flush out whatever was approved and merge. Most people use this with a lock file (and if you don't you are doing it wrong) and should not have any issues with new bugs showing up. This is already a high risk dependency which must be tested well before going into production. Let's take advantage of that and move faster.
|
||||
|
||||
Please do not harass people to review your pull request! You can tag those you feel have relevant experience but please don't abuse this as people will unfollow or mute the project if they are called too many times!
|
6
DRMType.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
WIDEVINE: 'widevine',
|
||||
PLAYREADY: 'playready',
|
||||
CLEARKEY: 'clearkey',
|
||||
FAIRPLAY: 'fairplay',
|
||||
};
|
@ -14,5 +14,5 @@ export default {
|
||||
PROCESS: 'CIPhotoEffectProcess',
|
||||
TONAL: 'CIPhotoEffectTonal',
|
||||
TRANSFER: 'CIPhotoEffectTransfer',
|
||||
SEPIA: 'CISepiaTone'
|
||||
SEPIA: 'CISepiaTone',
|
||||
};
|
||||
|
1
LICENSE
@ -1,5 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2022 Project contributors
|
||||
Copyright (c) 2016 Brent Vatne, Baris Sencan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
SRT: 'application/x-subrip',
|
||||
TTML: 'application/ttml+xml',
|
||||
VTT: 'text/vtt'
|
||||
VTT: 'text/vtt',
|
||||
};
|
||||
|
184
Video.js
@ -1,9 +1,11 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle} from 'react-native';
|
||||
import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native';
|
||||
import { ViewPropTypes, ImagePropTypes } from 'deprecated-react-native-prop-types';
|
||||
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
||||
import TextTrackType from './TextTrackType';
|
||||
import FilterType from './FilterType';
|
||||
import DRMType from './DRMType';
|
||||
import VideoResizeMode from './VideoResizeMode.js';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@ -12,7 +14,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export { TextTrackType, FilterType };
|
||||
const { VideoDecoderProperties } = NativeModules
|
||||
export { TextTrackType, FilterType, DRMType, VideoDecoderProperties }
|
||||
|
||||
export default class Video extends Component {
|
||||
|
||||
@ -20,24 +23,24 @@ export default class Video extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showPoster: !!props.poster
|
||||
showPoster: !!props.poster,
|
||||
};
|
||||
}
|
||||
|
||||
setNativeProps(nativeProps) {
|
||||
this._root.setNativeProps(nativeProps);
|
||||
}
|
||||
|
||||
|
||||
toTypeString(x) {
|
||||
switch (typeof x) {
|
||||
case "object":
|
||||
return x instanceof Date
|
||||
? x.toISOString()
|
||||
case 'object':
|
||||
return x instanceof Date
|
||||
? x.toISOString()
|
||||
: JSON.stringify(x); // object, null
|
||||
case "undefined":
|
||||
return "";
|
||||
case 'undefined':
|
||||
return '';
|
||||
default: // boolean, number, string
|
||||
return x.toString();
|
||||
return x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,14 +55,14 @@ export default class Video extends Component {
|
||||
}
|
||||
|
||||
seek = (time, tolerance = 100) => {
|
||||
if (isNaN(time)) throw new Error('Specified time is not a number');
|
||||
|
||||
if (isNaN(time)) {throw new Error('Specified time is not a number');}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
this.setNativeProps({
|
||||
seek: {
|
||||
time,
|
||||
tolerance
|
||||
}
|
||||
tolerance,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setNativeProps({ seek: time });
|
||||
@ -88,7 +91,7 @@ export default class Video extends Component {
|
||||
|
||||
_hidePoster = () => {
|
||||
if (this.state.showPoster) {
|
||||
this.setState({showPoster: false});
|
||||
this.setState({ showPoster: false });
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +101,12 @@ export default class Video extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
_onPlaybackStateChanged = (event) => {
|
||||
if (this.props.onPlaybackStateChanged) {
|
||||
this.props.onPlaybackStateChanged(event.nativeEvent);
|
||||
}
|
||||
};
|
||||
|
||||
_onLoad = (event) => {
|
||||
// Need to hide poster here for windows as onReadyForDisplay is not implemented
|
||||
if (Platform.OS === 'windows') {
|
||||
@ -124,7 +133,7 @@ export default class Video extends Component {
|
||||
if (this.props.onBandwidthUpdate) {
|
||||
this.props.onBandwidthUpdate(event.nativeEvent);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
_onSeek = (event) => {
|
||||
if (this.props.onSeek) {
|
||||
@ -169,7 +178,10 @@ export default class Video extends Component {
|
||||
};
|
||||
|
||||
_onReadyForDisplay = (event) => {
|
||||
this._hidePoster();
|
||||
if (!this.props.audioOnly) {
|
||||
this._hidePoster();
|
||||
}
|
||||
|
||||
if (this.props.onReadyForDisplay) {
|
||||
this.props.onReadyForDisplay(event.nativeEvent);
|
||||
}
|
||||
@ -192,7 +204,7 @@ export default class Video extends Component {
|
||||
this.props.onPlaybackRateChange(event.nativeEvent);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
_onExternalPlaybackChange = (event) => {
|
||||
if (this.props.onExternalPlaybackChange) {
|
||||
this.props.onExternalPlaybackChange(event.nativeEvent);
|
||||
@ -212,7 +224,7 @@ export default class Video extends Component {
|
||||
};
|
||||
|
||||
_onRestoreUserInterfaceForPictureInPictureStop = (event) => {
|
||||
if (this.props.onRestoreUserInterfaceForPictureInPictureStop) {
|
||||
if (this.props.onRestoreUserInterfaceForPictureInPictureStop) {
|
||||
this.props.onRestoreUserInterfaceForPictureInPictureStop();
|
||||
}
|
||||
};
|
||||
@ -229,29 +241,55 @@ export default class Video extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
getViewManagerConfig = viewManagerName => {
|
||||
if (!NativeModules.UIManager.getViewManagerConfig) {
|
||||
return NativeModules.UIManager[viewManagerName];
|
||||
_onGetLicense = (event) => {
|
||||
if (this.props.drm && this.props.drm.getLicense instanceof Function) {
|
||||
const data = event.nativeEvent;
|
||||
if (data && data.spcBase64) {
|
||||
const getLicenseOverride = this.props.drm.getLicense(data.spcBase64, data.contentId, data.licenseUrl);
|
||||
const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not.
|
||||
getLicensePromise.then((result => {
|
||||
if (result !== undefined) {
|
||||
NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root));
|
||||
} else {
|
||||
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root));
|
||||
}
|
||||
})).catch((error) => {
|
||||
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root));
|
||||
});
|
||||
} else {
|
||||
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', findNodeHandle(this._root));
|
||||
}
|
||||
}
|
||||
return NativeModules.UIManager.getViewManagerConfig(viewManagerName);
|
||||
}
|
||||
getViewManagerConfig = viewManagerName => {
|
||||
if (!UIManager.getViewManagerConfig) {
|
||||
return UIManager[viewManagerName];
|
||||
}
|
||||
return UIManager.getViewManagerConfig(viewManagerName);
|
||||
};
|
||||
|
||||
render() {
|
||||
const resizeMode = this.props.resizeMode;
|
||||
const source = resolveAssetSource(this.props.source) || {};
|
||||
const shouldCache = !Boolean(source.__packager_asset)
|
||||
const shouldCache = !source.__packager_asset;
|
||||
|
||||
let uri = source.uri || '';
|
||||
if (uri && uri.match(/^\//)) {
|
||||
uri = `file://${uri}`;
|
||||
}
|
||||
|
||||
|
||||
if (!uri) {
|
||||
console.warn('Trying to load empty source.');
|
||||
console.log('Trying to load empty source.');
|
||||
}
|
||||
|
||||
const isNetwork = !!(uri && uri.match(/^https?:/));
|
||||
const isAsset = !!(uri && uri.match(/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/));
|
||||
const isNetwork = !!(uri && uri.match(/^https?:/i));
|
||||
const isAsset = !!(uri && uri.match(/^(assets-library|ph|ipod-library|file|content|ms-appx|ms-appdata):/i));
|
||||
|
||||
if ((uri || uri === '') && !isNetwork && !isAsset) {
|
||||
if (this.props.onError) {
|
||||
this.props.onError({error: {errorString: 'invalid url, player will stop', errorCode: 'INVALID_URL'}});
|
||||
}
|
||||
}
|
||||
|
||||
let nativeResizeMode;
|
||||
const RCTVideoInstance = this.getViewManagerConfig('RCTVideo');
|
||||
@ -278,9 +316,10 @@ export default class Video extends Component {
|
||||
type: source.type || '',
|
||||
mainVer: source.mainVer || 0,
|
||||
patchVer: source.patchVer || 0,
|
||||
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}
|
||||
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
|
||||
},
|
||||
onVideoLoadStart: this._onLoadStart,
|
||||
onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
|
||||
onVideoLoad: this._onLoad,
|
||||
onVideoError: this._onError,
|
||||
onVideoProgress: this._onProgress,
|
||||
@ -301,6 +340,7 @@ export default class Video extends Component {
|
||||
onPlaybackRateChange: this._onPlaybackRateChange,
|
||||
onAudioFocusChanged: this._onAudioFocusChanged,
|
||||
onAudioBecomingNoisy: this._onAudioBecomingNoisy,
|
||||
onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense,
|
||||
onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged,
|
||||
onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop,
|
||||
});
|
||||
@ -327,29 +367,29 @@ export default class Video extends Component {
|
||||
|
||||
Video.propTypes = {
|
||||
filter: PropTypes.oneOf([
|
||||
FilterType.NONE,
|
||||
FilterType.INVERT,
|
||||
FilterType.MONOCHROME,
|
||||
FilterType.POSTERIZE,
|
||||
FilterType.FALSE,
|
||||
FilterType.MAXIMUMCOMPONENT,
|
||||
FilterType.MINIMUMCOMPONENT,
|
||||
FilterType.CHROME,
|
||||
FilterType.FADE,
|
||||
FilterType.INSTANT,
|
||||
FilterType.MONO,
|
||||
FilterType.NOIR,
|
||||
FilterType.PROCESS,
|
||||
FilterType.TONAL,
|
||||
FilterType.TRANSFER,
|
||||
FilterType.SEPIA
|
||||
FilterType.NONE,
|
||||
FilterType.INVERT,
|
||||
FilterType.MONOCHROME,
|
||||
FilterType.POSTERIZE,
|
||||
FilterType.FALSE,
|
||||
FilterType.MAXIMUMCOMPONENT,
|
||||
FilterType.MINIMUMCOMPONENT,
|
||||
FilterType.CHROME,
|
||||
FilterType.FADE,
|
||||
FilterType.INSTANT,
|
||||
FilterType.MONO,
|
||||
FilterType.NOIR,
|
||||
FilterType.PROCESS,
|
||||
FilterType.TONAL,
|
||||
FilterType.TRANSFER,
|
||||
FilterType.SEPIA,
|
||||
]),
|
||||
filterEnabled: PropTypes.bool,
|
||||
/* Native only */
|
||||
src: PropTypes.object,
|
||||
seek: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.object
|
||||
PropTypes.object,
|
||||
]),
|
||||
fullscreen: PropTypes.bool,
|
||||
onVideoLoadStart: PropTypes.func,
|
||||
@ -371,16 +411,27 @@ Video.propTypes = {
|
||||
/* Wrapper component */
|
||||
source: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
uri: PropTypes.string
|
||||
uri: PropTypes.string,
|
||||
}),
|
||||
// Opaque type returned by require('./video.mp4')
|
||||
PropTypes.number
|
||||
PropTypes.number,
|
||||
]),
|
||||
drm: PropTypes.shape({
|
||||
type: PropTypes.oneOf([
|
||||
DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY,
|
||||
]),
|
||||
licenseServer: PropTypes.string,
|
||||
headers: PropTypes.shape({}),
|
||||
base64Certificate: PropTypes.bool,
|
||||
certificateUrl: PropTypes.string,
|
||||
getLicense: PropTypes.func,
|
||||
}),
|
||||
localSourceEncryptionKeyScheme: PropTypes.string,
|
||||
minLoadRetryCount: PropTypes.number,
|
||||
maxBitRate: PropTypes.number,
|
||||
resizeMode: PropTypes.string,
|
||||
poster: PropTypes.string,
|
||||
posterResizeMode: Image.propTypes.resizeMode,
|
||||
posterResizeMode: ImagePropTypes.resizeMode,
|
||||
repeat: PropTypes.bool,
|
||||
automaticallyWaitsToMinimizeStalling: PropTypes.bool,
|
||||
allowsExternalPlayback: PropTypes.bool,
|
||||
@ -388,22 +439,22 @@ Video.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
PropTypes.number,
|
||||
]),
|
||||
}),
|
||||
selectedVideoTrack: PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
}),
|
||||
PropTypes.number,
|
||||
]),
|
||||
}),
|
||||
selectedTextTrack: PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
PropTypes.number,
|
||||
]),
|
||||
}),
|
||||
textTracks: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@ -414,7 +465,7 @@ Video.propTypes = {
|
||||
TextTrackType.TTML,
|
||||
TextTrackType.VTT,
|
||||
]),
|
||||
language: PropTypes.string.isRequired
|
||||
language: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
paused: PropTypes.bool,
|
||||
@ -425,24 +476,37 @@ Video.propTypes = {
|
||||
maxBufferMs: PropTypes.number,
|
||||
bufferForPlaybackMs: PropTypes.number,
|
||||
bufferForPlaybackAfterRebufferMs: PropTypes.number,
|
||||
maxHeapAllocationPercent: PropTypes.number,
|
||||
}),
|
||||
stereoPan: PropTypes.number,
|
||||
rate: PropTypes.number,
|
||||
pictureInPicture: PropTypes.bool,
|
||||
playInBackground: PropTypes.bool,
|
||||
preferredForwardBufferDuration: PropTypes.number,
|
||||
playWhenInactive: PropTypes.bool,
|
||||
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
|
||||
reportBandwidth: PropTypes.bool,
|
||||
contentStartTime: PropTypes.number,
|
||||
disableFocus: PropTypes.bool,
|
||||
focusable: PropTypes.bool,
|
||||
disableBuffering: PropTypes.bool,
|
||||
controls: PropTypes.bool,
|
||||
audioOnly: PropTypes.bool,
|
||||
currentTime: PropTypes.number,
|
||||
fullscreenAutorotate: PropTypes.bool,
|
||||
fullscreenOrientation: PropTypes.oneOf(['all','landscape','portrait']),
|
||||
fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']),
|
||||
progressUpdateInterval: PropTypes.number,
|
||||
subtitleStyle: PropTypes.shape({
|
||||
paddingTop: PropTypes.number,
|
||||
paddingBottom: PropTypes.number,
|
||||
paddingLeft: PropTypes.number,
|
||||
paddingRight: PropTypes.number,
|
||||
fontSize: PropTypes.number,
|
||||
}),
|
||||
useTextureView: PropTypes.bool,
|
||||
useSecureView: PropTypes.bool,
|
||||
hideShutterView: PropTypes.bool,
|
||||
onLoadStart: PropTypes.func,
|
||||
onPlaybackStateChanged: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
onBuffer: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
|
@ -1 +0,0 @@
|
||||
M Video.js
|
@ -1,40 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 28)
|
||||
buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet('minSdkVersion', 16)
|
||||
targetSdkVersion safeExtGet('targetSdkVersion', 28)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.10.4') {
|
||||
exclude group: 'com.android.support'
|
||||
}
|
||||
|
||||
// All support libs must use the same version
|
||||
implementation "androidx.annotation:annotation:1.1.0"
|
||||
implementation "androidx.core:core:1.1.0"
|
||||
implementation "androidx.media:media:1.1.0"
|
||||
|
||||
implementation('com.google.android.exoplayer:extension-okhttp:2.10.4') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
}
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.3'
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.brentvatne.react">
|
||||
</manifest>
|
@ -1,44 +0,0 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import com.brentvatne.exoplayer.DefaultReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactVideoPackage implements ReactPackage {
|
||||
|
||||
private ReactExoplayerConfig config;
|
||||
|
||||
public ReactVideoPackage() {
|
||||
}
|
||||
|
||||
public ReactVideoPackage(ReactExoplayerConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Deprecated RN 0.47
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
if (config == null) {
|
||||
config = new DefaultReactExoplayerConfig(reactContext);
|
||||
}
|
||||
return Collections.singletonList(new ReactExoplayerViewManager(config));
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ https://github.com/google/ExoPlayer
|
||||
## Benefits over `react-native-video@0.9.0`:
|
||||
|
||||
- Android Video library built by Google, with a lot of support
|
||||
- Supports DASH, HlS, & SmoothStreaming adaptive streams
|
||||
- Supports DASH, HLS, & SmoothStreaming adaptive streams
|
||||
- Supports formats such as MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC).
|
||||
- Fewer device specific issues
|
||||
- Highly customisable
|
@ -5,22 +5,45 @@ def safeExtGet(prop, fallback) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 28)
|
||||
buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2')
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet('minSdkVersion', 16)
|
||||
minSdkVersion safeExtGet('minSdkVersion', 21)
|
||||
targetSdkVersion safeExtGet('targetSdkVersion', 28)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation 'com.yqritc:android-scalablevideoview:1.0.4'
|
||||
repositories {
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.18.1') {
|
||||
exclude group: 'com.android.support'
|
||||
}
|
||||
|
||||
// All support libs must use the same version
|
||||
implementation "androidx.annotation:annotation:1.1.0"
|
||||
implementation "androidx.core:core:1.1.0"
|
||||
implementation "androidx.media:media:1.1.0"
|
||||
implementation "androidx.activity:activity:1.4.0"
|
||||
|
||||
implementation('com.google.android.exoplayer:extension-okhttp:2.18.1') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
}
|
||||
implementation "com.squareup.okhttp3:okhttp:" + '$OKHTTP_VERSION'
|
||||
}
|
||||
|
@ -1,287 +0,0 @@
|
||||
package com.android.vending.expansion.zipfile;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//To implement APEZProvider in your application, you'll want to change
|
||||
//the AUTHORITY to match what you define in the manifest.
|
||||
|
||||
import com.android.vending.expansion.zipfile.ZipResourceFile.ZipEntryRO;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderResult;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This content provider is an optional part of the library.
|
||||
*
|
||||
* <p>Most apps don't need to use this class. This defines a
|
||||
* ContentProvider that marshalls the data from the ZIP files through a
|
||||
* content provider Uri in order to provide file access for certain Android APIs
|
||||
* that expect Uri access to media files.
|
||||
*
|
||||
*/
|
||||
public abstract class APEZProvider extends ContentProvider {
|
||||
|
||||
private ZipResourceFile mAPKExtensionFile;
|
||||
private boolean mInit;
|
||||
|
||||
public static final String FILEID = BaseColumns._ID;
|
||||
public static final String FILENAME = "ZPFN";
|
||||
public static final String ZIPFILE = "ZFIL";
|
||||
public static final String MODIFICATION = "ZMOD";
|
||||
public static final String CRC32 = "ZCRC";
|
||||
public static final String COMPRESSEDLEN = "ZCOL";
|
||||
public static final String UNCOMPRESSEDLEN = "ZUNL";
|
||||
public static final String COMPRESSIONTYPE = "ZTYP";
|
||||
|
||||
public static final String[] ALL_FIELDS = {
|
||||
FILEID,
|
||||
FILENAME,
|
||||
ZIPFILE,
|
||||
MODIFICATION,
|
||||
CRC32,
|
||||
COMPRESSEDLEN,
|
||||
UNCOMPRESSEDLEN,
|
||||
COMPRESSIONTYPE
|
||||
};
|
||||
|
||||
public static final int FILEID_IDX = 0;
|
||||
public static final int FILENAME_IDX = 1;
|
||||
public static final int ZIPFILE_IDX = 2;
|
||||
public static final int MOD_IDX = 3;
|
||||
public static final int CRC_IDX = 4;
|
||||
public static final int COMPLEN_IDX = 5;
|
||||
public static final int UNCOMPLEN_IDX = 6;
|
||||
public static final int COMPTYPE_IDX = 7;
|
||||
|
||||
public static final int[] ALL_FIELDS_INT = {
|
||||
FILEID_IDX,
|
||||
FILENAME_IDX,
|
||||
ZIPFILE_IDX,
|
||||
MOD_IDX,
|
||||
CRC_IDX,
|
||||
COMPLEN_IDX,
|
||||
UNCOMPLEN_IDX,
|
||||
COMPTYPE_IDX
|
||||
};
|
||||
|
||||
/**
|
||||
* This needs to match the authority in your manifest
|
||||
*/
|
||||
public abstract String getAuthority();
|
||||
|
||||
@Override
|
||||
public int delete(Uri arg0, String arg1, String[] arg2) {
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return "vnd.android.cursor.item/asset";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
static private final String NO_FILE = "N";
|
||||
|
||||
private boolean initIfNecessary() {
|
||||
if ( !mInit ) {
|
||||
Context ctx = getContext();
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
ProviderInfo pi = pm.resolveContentProvider(getAuthority(), PackageManager.GET_META_DATA);
|
||||
PackageInfo packInfo;
|
||||
try {
|
||||
packInfo = pm.getPackageInfo(ctx.getPackageName(), 0);
|
||||
} catch (NameNotFoundException e1) {
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
int patchFileVersion;
|
||||
int mainFileVersion;
|
||||
int appVersionCode = packInfo.versionCode;
|
||||
String[] resourceFiles = null;
|
||||
if ( null != pi.metaData ) {
|
||||
mainFileVersion = pi.metaData.getInt("mainVersion", appVersionCode);
|
||||
patchFileVersion = pi.metaData.getInt("patchVersion", appVersionCode);
|
||||
String mainFileName = pi.metaData.getString("mainFilename", NO_FILE);
|
||||
if ( NO_FILE != mainFileName ) {
|
||||
String patchFileName = pi.metaData.getString("patchFilename", NO_FILE);
|
||||
if ( NO_FILE != patchFileName ) {
|
||||
resourceFiles = new String[] { mainFileName, patchFileName };
|
||||
} else {
|
||||
resourceFiles = new String[] { mainFileName };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mainFileVersion = patchFileVersion = appVersionCode;
|
||||
}
|
||||
try {
|
||||
if ( null == resourceFiles ) {
|
||||
mAPKExtensionFile = APKExpansionSupport.getAPKExpansionZipFile(ctx, mainFileVersion, patchFileVersion);
|
||||
} else {
|
||||
mAPKExtensionFile = APKExpansionSupport.getResourceZipFile(resourceFiles);
|
||||
}
|
||||
mInit = true;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openAssetFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
initIfNecessary();
|
||||
String path = uri.getEncodedPath();
|
||||
if ( path.startsWith("/") ) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
return mAPKExtensionFile.getAssetFileDescriptor(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentProviderResult[] applyBatch(
|
||||
ArrayList<ContentProviderOperation> operations)
|
||||
throws OperationApplicationException {
|
||||
initIfNecessary();
|
||||
return super.applyBatch(operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
initIfNecessary();
|
||||
AssetFileDescriptor af = openAssetFile(uri, mode);
|
||||
if ( null != af ) {
|
||||
return af.getParcelFileDescriptor();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
initIfNecessary();
|
||||
// lists all of the items in the file that match
|
||||
ZipEntryRO[] zipEntries;
|
||||
if ( null == mAPKExtensionFile ) {
|
||||
zipEntries = new ZipEntryRO[0];
|
||||
} else {
|
||||
zipEntries = mAPKExtensionFile.getAllEntries();
|
||||
}
|
||||
int[] intProjection;
|
||||
if ( null == projection ) {
|
||||
intProjection = ALL_FIELDS_INT;
|
||||
projection = ALL_FIELDS;
|
||||
} else {
|
||||
int len = projection.length;
|
||||
intProjection = new int[len];
|
||||
for ( int i = 0; i < len; i++ ) {
|
||||
if ( projection[i].equals(FILEID) ) {
|
||||
intProjection[i] = FILEID_IDX;
|
||||
} else if ( projection[i].equals(FILENAME) ) {
|
||||
intProjection[i] = FILENAME_IDX;
|
||||
} else if ( projection[i].equals(ZIPFILE) ) {
|
||||
intProjection[i] = ZIPFILE_IDX;
|
||||
} else if ( projection[i].equals(MODIFICATION) ) {
|
||||
intProjection[i] = MOD_IDX;
|
||||
} else if ( projection[i].equals(CRC32) ) {
|
||||
intProjection[i] = CRC_IDX;
|
||||
} else if ( projection[i].equals(COMPRESSEDLEN) ) {
|
||||
intProjection[i] = COMPLEN_IDX;
|
||||
} else if ( projection[i].equals(UNCOMPRESSEDLEN) ) {
|
||||
intProjection[i] = UNCOMPLEN_IDX;
|
||||
} else if ( projection[i].equals(COMPRESSIONTYPE) ) {
|
||||
intProjection[i] = COMPTYPE_IDX;
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
MatrixCursor mc = new MatrixCursor(projection, zipEntries.length);
|
||||
int len = intProjection.length;
|
||||
for ( ZipEntryRO zer : zipEntries ) {
|
||||
MatrixCursor.RowBuilder rb = mc.newRow();
|
||||
for ( int i = 0; i < len; i++ ) {
|
||||
switch (intProjection[i]) {
|
||||
case FILEID_IDX:
|
||||
rb.add(i);
|
||||
break;
|
||||
case FILENAME_IDX:
|
||||
rb.add(zer.mFileName);
|
||||
break;
|
||||
case ZIPFILE_IDX:
|
||||
rb.add(zer.getZipFileName());
|
||||
break;
|
||||
case MOD_IDX:
|
||||
rb.add(zer.mWhenModified);
|
||||
break;
|
||||
case CRC_IDX:
|
||||
rb.add(zer.mCRC32);
|
||||
break;
|
||||
case COMPLEN_IDX:
|
||||
rb.add(zer.mCompressedLength);
|
||||
break;
|
||||
case UNCOMPLEN_IDX:
|
||||
rb.add(zer.mUncompressedLength);
|
||||
break;
|
||||
case COMPTYPE_IDX:
|
||||
rb.add(zer.mMethod);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package com.android.vending.expansion.zipfile;
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
public class APKExpansionSupport {
|
||||
// The shared path to all app expansion files
|
||||
private final static String EXP_PATH = "/Android/obb/";
|
||||
|
||||
static String[] getAPKExpansionFiles(Context ctx, int mainVersion, int patchVersion) {
|
||||
String packageName = ctx.getPackageName();
|
||||
Vector<String> ret = new Vector<String>();
|
||||
if (Environment.getExternalStorageState().equals(
|
||||
Environment.MEDIA_MOUNTED)) {
|
||||
// Build the full path to the app's expansion files
|
||||
File root = Environment.getExternalStorageDirectory();
|
||||
File expPath = new File(root.toString() + EXP_PATH + packageName);
|
||||
|
||||
// Check that expansion file path exists
|
||||
if (expPath.exists()) {
|
||||
if ( mainVersion > 0 ) {
|
||||
String strMainPath = expPath + File.separator + "main." + mainVersion + "." + packageName + ".obb";
|
||||
// Log.d("APKEXPANSION", strMainPath);
|
||||
File main = new File(strMainPath);
|
||||
if ( main.isFile() ) {
|
||||
ret.add(strMainPath);
|
||||
}
|
||||
}
|
||||
if ( patchVersion > 0 ) {
|
||||
String strPatchPath = expPath + File.separator + "patch." + patchVersion + "." + packageName + ".obb";
|
||||
File main = new File(strPatchPath);
|
||||
if ( main.isFile() ) {
|
||||
ret.add(strPatchPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String[] retArray = new String[ret.size()];
|
||||
ret.toArray(retArray);
|
||||
return retArray;
|
||||
}
|
||||
|
||||
static public ZipResourceFile getResourceZipFile(String[] expansionFiles) throws IOException {
|
||||
ZipResourceFile apkExpansionFile = null;
|
||||
for (String expansionFilePath : expansionFiles) {
|
||||
if ( null == apkExpansionFile ) {
|
||||
apkExpansionFile = new ZipResourceFile(expansionFilePath);
|
||||
} else {
|
||||
apkExpansionFile.addPatchFile(expansionFilePath);
|
||||
}
|
||||
}
|
||||
return apkExpansionFile;
|
||||
}
|
||||
|
||||
static public ZipResourceFile getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion) throws IOException{
|
||||
String[] expansionFiles = getAPKExpansionFiles(ctx, mainVersion, patchVersion);
|
||||
return getResourceZipFile(expansionFiles);
|
||||
}
|
||||
}
|
@ -1,428 +0,0 @@
|
||||
|
||||
package com.android.vending.expansion.zipfile;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Vector;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ZipResourceFile {
|
||||
|
||||
//
|
||||
// Read-only access to Zip archives, with minimal heap allocation.
|
||||
//
|
||||
static final String LOG_TAG = "zipro";
|
||||
static final boolean LOGV = false;
|
||||
|
||||
// 4-byte number
|
||||
static private int swapEndian(int i)
|
||||
{
|
||||
return ((i & 0xff) << 24) + ((i & 0xff00) << 8) + ((i & 0xff0000) >>> 8)
|
||||
+ ((i >>> 24) & 0xff);
|
||||
}
|
||||
|
||||
// 2-byte number
|
||||
static private int swapEndian(short i)
|
||||
{
|
||||
return ((i & 0x00FF) << 8 | (i & 0xFF00) >>> 8);
|
||||
}
|
||||
|
||||
/*
|
||||
* Zip file constants.
|
||||
*/
|
||||
static final int kEOCDSignature = 0x06054b50;
|
||||
static final int kEOCDLen = 22;
|
||||
static final int kEOCDNumEntries = 8; // offset to #of entries in file
|
||||
static final int kEOCDSize = 12; // size of the central directory
|
||||
static final int kEOCDFileOffset = 16; // offset to central directory
|
||||
|
||||
static final int kMaxCommentLen = 65535; // longest possible in ushort
|
||||
static final int kMaxEOCDSearch = (kMaxCommentLen + kEOCDLen);
|
||||
|
||||
static final int kLFHSignature = 0x04034b50;
|
||||
static final int kLFHLen = 30; // excluding variable-len fields
|
||||
static final int kLFHNameLen = 26; // offset to filename length
|
||||
static final int kLFHExtraLen = 28; // offset to extra length
|
||||
|
||||
static final int kCDESignature = 0x02014b50;
|
||||
static final int kCDELen = 46; // excluding variable-len fields
|
||||
static final int kCDEMethod = 10; // offset to compression method
|
||||
static final int kCDEModWhen = 12; // offset to modification timestamp
|
||||
static final int kCDECRC = 16; // offset to entry CRC
|
||||
static final int kCDECompLen = 20; // offset to compressed length
|
||||
static final int kCDEUncompLen = 24; // offset to uncompressed length
|
||||
static final int kCDENameLen = 28; // offset to filename length
|
||||
static final int kCDEExtraLen = 30; // offset to extra length
|
||||
static final int kCDECommentLen = 32; // offset to comment length
|
||||
static final int kCDELocalOffset = 42; // offset to local hdr
|
||||
|
||||
static final int kCompressStored = 0; // no compression
|
||||
static final int kCompressDeflated = 8; // standard deflate
|
||||
|
||||
/*
|
||||
* The values we return for ZipEntryRO use 0 as an invalid value, so we want
|
||||
* to adjust the hash table index by a fixed amount. Using a large value
|
||||
* helps insure that people don't mix & match arguments, e.g. to
|
||||
* findEntryByIndex().
|
||||
*/
|
||||
static final int kZipEntryAdj = 10000;
|
||||
|
||||
static public final class ZipEntryRO {
|
||||
public ZipEntryRO(final String zipFileName, final File file, final String fileName) {
|
||||
mFileName = fileName;
|
||||
mZipFileName = zipFileName;
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
public final File mFile;
|
||||
public final String mFileName;
|
||||
public final String mZipFileName;
|
||||
public long mLocalHdrOffset; // offset of local file header
|
||||
|
||||
/* useful stuff from the directory entry */
|
||||
public int mMethod;
|
||||
public long mWhenModified;
|
||||
public long mCRC32;
|
||||
public long mCompressedLength;
|
||||
public long mUncompressedLength;
|
||||
|
||||
public long mOffset = -1;
|
||||
|
||||
public void setOffsetFromFile(RandomAccessFile f, ByteBuffer buf) throws IOException {
|
||||
long localHdrOffset = mLocalHdrOffset;
|
||||
try {
|
||||
f.seek(localHdrOffset);
|
||||
f.readFully(buf.array());
|
||||
if (buf.getInt(0) != kLFHSignature) {
|
||||
Log.w(LOG_TAG, "didn't find signature at start of lfh");
|
||||
throw new IOException();
|
||||
}
|
||||
int nameLen = buf.getShort(kLFHNameLen) & 0xFFFF;
|
||||
int extraLen = buf.getShort(kLFHExtraLen) & 0xFFFF;
|
||||
mOffset = localHdrOffset + kLFHLen + nameLen + extraLen;
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the offset of the start of the Zip file entry within the
|
||||
* Zip file.
|
||||
*
|
||||
* @return the offset, in bytes from the start of the file of the entry
|
||||
*/
|
||||
public long getOffset() {
|
||||
return mOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* isUncompressed
|
||||
*
|
||||
* @return true if the file is stored in uncompressed form
|
||||
*/
|
||||
public boolean isUncompressed() {
|
||||
return mMethod == kCompressStored;
|
||||
}
|
||||
|
||||
public AssetFileDescriptor getAssetFileDescriptor() {
|
||||
if (mMethod == kCompressStored) {
|
||||
ParcelFileDescriptor pfd;
|
||||
try {
|
||||
pfd = ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
return new AssetFileDescriptor(pfd, getOffset(), mUncompressedLength);
|
||||
} catch (FileNotFoundException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getZipFileName() {
|
||||
return mZipFileName;
|
||||
}
|
||||
|
||||
public File getZipFile() {
|
||||
return mFile;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private HashMap<String, ZipEntryRO> mHashMap = new HashMap<String, ZipEntryRO>();
|
||||
|
||||
/* for reading compressed files */
|
||||
public HashMap<File, ZipFile> mZipFiles = new HashMap<File, ZipFile>();
|
||||
|
||||
public ZipResourceFile(String zipFileName) throws IOException {
|
||||
addPatchFile(zipFileName);
|
||||
}
|
||||
|
||||
ZipEntryRO[] getEntriesAt(String path) {
|
||||
Vector<ZipEntryRO> zev = new Vector<ZipEntryRO>();
|
||||
Collection<ZipEntryRO> values = mHashMap.values();
|
||||
if (null == path)
|
||||
path = "";
|
||||
int length = path.length();
|
||||
for (ZipEntryRO ze : values) {
|
||||
if (ze.mFileName.startsWith(path)) {
|
||||
if (-1 == ze.mFileName.indexOf('/', length)) {
|
||||
zev.add(ze);
|
||||
}
|
||||
}
|
||||
}
|
||||
ZipEntryRO[] entries = new ZipEntryRO[zev.size()];
|
||||
return zev.toArray(entries);
|
||||
}
|
||||
|
||||
public ZipEntryRO[] getAllEntries() {
|
||||
Collection<ZipEntryRO> values = mHashMap.values();
|
||||
return values.toArray(new ZipEntryRO[values.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* getAssetFileDescriptor allows for ZipResourceFile to directly feed
|
||||
* Android API's that want an fd, offset, and length such as the
|
||||
* MediaPlayer. It also allows for the class to be used in a content
|
||||
* provider that can feed video players. The file must be stored
|
||||
* (non-compressed) in the Zip file for this to work.
|
||||
*
|
||||
* @param assetPath
|
||||
* @return the asset file descriptor for the file, or null if the file isn't
|
||||
* present or is stored compressed
|
||||
*/
|
||||
public AssetFileDescriptor getAssetFileDescriptor(String assetPath) {
|
||||
ZipEntryRO entry = mHashMap.get(assetPath);
|
||||
if (null != entry) {
|
||||
return entry.getAssetFileDescriptor();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getInputStream returns an AssetFileDescriptor.AutoCloseInputStream
|
||||
* associated with the asset that is contained in the Zip file, or a
|
||||
* standard ZipInputStream if necessary to uncompress the file
|
||||
*
|
||||
* @param assetPath
|
||||
* @return an input stream for the named asset path, or null if not found
|
||||
* @throws IOException
|
||||
*/
|
||||
public InputStream getInputStream(String assetPath) throws IOException {
|
||||
ZipEntryRO entry = mHashMap.get(assetPath);
|
||||
if (null != entry) {
|
||||
if (entry.isUncompressed()) {
|
||||
return entry.getAssetFileDescriptor().createInputStream();
|
||||
} else {
|
||||
ZipFile zf = mZipFiles.get(entry.getZipFile());
|
||||
/** read compressed files **/
|
||||
if (null == zf) {
|
||||
zf = new ZipFile(entry.getZipFile(), ZipFile.OPEN_READ);
|
||||
mZipFiles.put(entry.getZipFile(), zf);
|
||||
}
|
||||
ZipEntry zi = zf.getEntry(assetPath);
|
||||
if (null != zi)
|
||||
return zf.getInputStream(zi);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer mLEByteBuffer = ByteBuffer.allocate(4);
|
||||
|
||||
static private int read4LE(RandomAccessFile f) throws EOFException, IOException {
|
||||
return swapEndian(f.readInt());
|
||||
}
|
||||
|
||||
/*
|
||||
* Opens the specified file read-only. We memory-map the entire thing and
|
||||
* close the file before returning.
|
||||
*/
|
||||
void addPatchFile(String zipFileName) throws IOException
|
||||
{
|
||||
File file = new File(zipFileName);
|
||||
RandomAccessFile f = new RandomAccessFile(file, "r");
|
||||
long fileLength = f.length();
|
||||
|
||||
if (fileLength < kEOCDLen) {
|
||||
throw new java.io.IOException();
|
||||
}
|
||||
|
||||
long readAmount = kMaxEOCDSearch;
|
||||
if (readAmount > fileLength)
|
||||
readAmount = fileLength;
|
||||
|
||||
/*
|
||||
* Make sure this is a Zip archive.
|
||||
*/
|
||||
f.seek(0);
|
||||
|
||||
int header = read4LE(f);
|
||||
if (header == kEOCDSignature) {
|
||||
Log.i(LOG_TAG, "Found Zip archive, but it looks empty");
|
||||
throw new IOException();
|
||||
} else if (header != kLFHSignature) {
|
||||
Log.v(LOG_TAG, "Not a Zip archive");
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform the traditional EOCD snipe hunt. We're searching for the End
|
||||
* of Central Directory magic number, which appears at the start of the
|
||||
* EOCD block. It's followed by 18 bytes of EOCD stuff and up to 64KB of
|
||||
* archive comment. We need to read the last part of the file into a
|
||||
* buffer, dig through it to find the magic number, parse some values
|
||||
* out, and use those to determine the extent of the CD. We start by
|
||||
* pulling in the last part of the file.
|
||||
*/
|
||||
long searchStart = fileLength - readAmount;
|
||||
|
||||
f.seek(searchStart);
|
||||
ByteBuffer bbuf = ByteBuffer.allocate((int) readAmount);
|
||||
byte[] buffer = bbuf.array();
|
||||
f.readFully(buffer);
|
||||
bbuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
/*
|
||||
* Scan backward for the EOCD magic. In an archive without a trailing
|
||||
* comment, we'll find it on the first try. (We may want to consider
|
||||
* doing an initial minimal read; if we don't find it, retry with a
|
||||
* second read as above.)
|
||||
*/
|
||||
|
||||
// EOCD == 0x50, 0x4b, 0x05, 0x06
|
||||
int eocdIdx;
|
||||
for (eocdIdx = buffer.length - kEOCDLen; eocdIdx >= 0; eocdIdx--) {
|
||||
if (buffer[eocdIdx] == 0x50 && bbuf.getInt(eocdIdx) == kEOCDSignature)
|
||||
{
|
||||
if (LOGV) {
|
||||
Log.v(LOG_TAG, "+++ Found EOCD at index: " + eocdIdx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (eocdIdx < 0) {
|
||||
Log.d(LOG_TAG, "Zip: EOCD not found, " + zipFileName + " is not zip");
|
||||
}
|
||||
|
||||
/*
|
||||
* Grab the CD offset and size, and the number of entries in the
|
||||
* archive. After that, we can release our EOCD hunt buffer.
|
||||
*/
|
||||
|
||||
int numEntries = bbuf.getShort(eocdIdx + kEOCDNumEntries);
|
||||
long dirSize = bbuf.getInt(eocdIdx + kEOCDSize) & 0xffffffffL;
|
||||
long dirOffset = bbuf.getInt(eocdIdx + kEOCDFileOffset) & 0xffffffffL;
|
||||
|
||||
// Verify that they look reasonable.
|
||||
if (dirOffset + dirSize > fileLength) {
|
||||
Log.w(LOG_TAG, "bad offsets (dir " + dirOffset + ", size " + dirSize + ", eocd "
|
||||
+ eocdIdx + ")");
|
||||
throw new IOException();
|
||||
}
|
||||
if (numEntries == 0) {
|
||||
Log.w(LOG_TAG, "empty archive?");
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
if (LOGV) {
|
||||
Log.v(LOG_TAG, "+++ numEntries=" + numEntries + " dirSize=" + dirSize + " dirOffset="
|
||||
+ dirOffset);
|
||||
}
|
||||
|
||||
MappedByteBuffer directoryMap = f.getChannel()
|
||||
.map(FileChannel.MapMode.READ_ONLY, dirOffset, dirSize);
|
||||
directoryMap.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
byte[] tempBuf = new byte[0xffff];
|
||||
|
||||
/*
|
||||
* Walk through the central directory, adding entries to the hash table.
|
||||
*/
|
||||
|
||||
int currentOffset = 0;
|
||||
|
||||
/*
|
||||
* Allocate the local directory information
|
||||
*/
|
||||
ByteBuffer buf = ByteBuffer.allocate(kLFHLen);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
for (int i = 0; i < numEntries; i++) {
|
||||
if (directoryMap.getInt(currentOffset) != kCDESignature) {
|
||||
Log.w(LOG_TAG, "Missed a central dir sig (at " + currentOffset + ")");
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
/* useful stuff from the directory entry */
|
||||
int fileNameLen = directoryMap.getShort(currentOffset + kCDENameLen) & 0xffff;
|
||||
int extraLen = directoryMap.getShort(currentOffset + kCDEExtraLen) & 0xffff;
|
||||
int commentLen = directoryMap.getShort(currentOffset + kCDECommentLen) & 0xffff;
|
||||
|
||||
/* get the CDE filename */
|
||||
|
||||
directoryMap.position(currentOffset + kCDELen);
|
||||
directoryMap.get(tempBuf, 0, fileNameLen);
|
||||
directoryMap.position(0);
|
||||
|
||||
/* UTF-8 on Android */
|
||||
String str = new String(tempBuf, 0, fileNameLen);
|
||||
if (LOGV) {
|
||||
Log.v(LOG_TAG, "Filename: " + str);
|
||||
}
|
||||
|
||||
ZipEntryRO ze = new ZipEntryRO(zipFileName, file, str);
|
||||
ze.mMethod = directoryMap.getShort(currentOffset + kCDEMethod) & 0xffff;
|
||||
ze.mWhenModified = directoryMap.getInt(currentOffset + kCDEModWhen) & 0xffffffffL;
|
||||
ze.mCRC32 = directoryMap.getLong(currentOffset + kCDECRC) & 0xffffffffL;
|
||||
ze.mCompressedLength = directoryMap.getLong(currentOffset + kCDECompLen) & 0xffffffffL;
|
||||
ze.mUncompressedLength = directoryMap.getLong(currentOffset + kCDEUncompLen) & 0xffffffffL;
|
||||
ze.mLocalHdrOffset = directoryMap.getInt(currentOffset + kCDELocalOffset) & 0xffffffffL;
|
||||
|
||||
// set the offsets
|
||||
buf.clear();
|
||||
ze.setOffsetFromFile(f, buf);
|
||||
|
||||
// put file into hash
|
||||
mHashMap.put(str, ze);
|
||||
|
||||
// go to next directory entry
|
||||
currentOffset += kCDELen + fileNameLen + extraLen + commentLen;
|
||||
}
|
||||
if (LOGV) {
|
||||
Log.v(LOG_TAG, "+++ zip good scan " + numEntries + " entries");
|
||||
}
|
||||
}
|
||||
}
|
22
android/src/main/java/com/brentvatne/ReactBridgeUtils.java
Normal file
@ -0,0 +1,22 @@
|
||||
package com.brentvatne;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
/*
|
||||
* This file define static helpers to parse in an easier way input props
|
||||
*/
|
||||
public class ReactBridgeUtils {
|
||||
/*
|
||||
retrieve key from map as int. fallback is returned if not available
|
||||
*/
|
||||
static public int safeGetInt(ReadableMap map, String key, int fallback) {
|
||||
return map != null && map.hasKey(key) && !map.isNull(key) ? map.getInt(key) : fallback;
|
||||
}
|
||||
|
||||
/*
|
||||
retrieve key from map as double. fallback is returned if not available
|
||||
*/
|
||||
static public double safeGetDouble(ReadableMap map, String key, double fallback) {
|
||||
return map != null && map.hasKey(key) && !map.isNull(key) ? map.getDouble(key) : fallback;
|
||||
}
|
||||
}
|
@ -67,6 +67,10 @@ public final class AspectRatioFrameLayout extends FrameLayout {
|
||||
return videoAspectRatio;
|
||||
}
|
||||
|
||||
public void invalidateAspectRatio() {
|
||||
videoAspectRatio = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
@ -4,13 +4,14 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.CookieJarContainer;
|
||||
import com.facebook.react.modules.network.ForwardingCookieHandler;
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.JavaNetCookieJar;
|
||||
import okhttp3.OkHttpClient;
|
||||
import java.util.Map;
|
||||
@ -22,6 +23,7 @@ public class DataSourceUtil {
|
||||
|
||||
private static DataSource.Factory rawDataSourceFactory = null;
|
||||
private static DataSource.Factory defaultDataSourceFactory = null;
|
||||
private static HttpDataSource.Factory defaultHttpDataSourceFactory = null;
|
||||
private static String userAgent = null;
|
||||
|
||||
public static void setUserAgent(String userAgent) {
|
||||
@ -58,12 +60,23 @@ public class DataSourceUtil {
|
||||
DataSourceUtil.defaultDataSourceFactory = factory;
|
||||
}
|
||||
|
||||
public static HttpDataSource.Factory getDefaultHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
if (defaultHttpDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
|
||||
defaultHttpDataSourceFactory = buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders);
|
||||
}
|
||||
return defaultHttpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static void setDefaultHttpDataSourceFactory(HttpDataSource.Factory factory) {
|
||||
DataSourceUtil.defaultHttpDataSourceFactory = factory;
|
||||
}
|
||||
|
||||
private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) {
|
||||
return new RawResourceDataSourceFactory(context.getApplicationContext());
|
||||
}
|
||||
|
||||
private static DataSource.Factory buildDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
|
||||
return new DefaultDataSourceFactory(context, bandwidthMeter,
|
||||
return new DefaultDataSource.Factory(context,
|
||||
buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders));
|
||||
}
|
||||
|
||||
@ -72,10 +85,12 @@ public class DataSourceUtil {
|
||||
CookieJarContainer container = (CookieJarContainer) client.cookieJar();
|
||||
ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
|
||||
container.setCookieJar(new JavaNetCookieJar(handler));
|
||||
OkHttpDataSourceFactory okHttpDataSourceFactory = new OkHttpDataSourceFactory(client, getUserAgent(context), bandwidthMeter);
|
||||
OkHttpDataSource.Factory okHttpDataSourceFactory = new OkHttpDataSource.Factory((Call.Factory) client)
|
||||
.setUserAgent(getUserAgent(context))
|
||||
.setTransferListener(bandwidthMeter);
|
||||
|
||||
if (requestHeaders != null)
|
||||
okHttpDataSourceFactory.getDefaultRequestProperties().set(requestHeaders);
|
||||
okHttpDataSourceFactory.setDefaultRequestProperties(requestHeaders);
|
||||
|
||||
return okHttpDataSourceFactory;
|
||||
}
|
@ -9,16 +9,28 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
public class DefaultReactExoplayerConfig implements ReactExoplayerConfig {
|
||||
|
||||
private final DefaultBandwidthMeter bandwidthMeter;
|
||||
private boolean disableDisconnectError = false;
|
||||
|
||||
public DefaultReactExoplayerConfig(Context context) {
|
||||
this.bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount) {
|
||||
if (this.disableDisconnectError) {
|
||||
// Use custom error handling policy to prevent throwing an error when losing network connection
|
||||
return new ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount);
|
||||
}
|
||||
return new DefaultLoadErrorHandlingPolicy(minLoadRetryCount);
|
||||
}
|
||||
|
||||
public void setDisableDisconnectError(boolean disableDisconnectError) {
|
||||
this.disableDisconnectError = disableDisconnectError;
|
||||
}
|
||||
|
||||
public boolean getDisableDisconnectError() {
|
||||
return this.disableDisconnectError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultBandwidthMeter getBandwidthMeter() {
|
||||
return bandwidthMeter;
|
@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
@ -13,16 +14,16 @@ import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.Tracks;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.TextRenderer;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -34,11 +35,12 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
private final SubtitleView subtitleLayout;
|
||||
private final AspectRatioFrameLayout layout;
|
||||
private final ComponentListener componentListener;
|
||||
private SimpleExoPlayer player;
|
||||
private ExoPlayer player;
|
||||
private Context context;
|
||||
private ViewGroup.LayoutParams layoutParams;
|
||||
|
||||
private boolean useTextureView = true;
|
||||
private boolean useSecureView = false;
|
||||
private boolean hideShutterView = false;
|
||||
|
||||
public ExoPlayerView(Context context) {
|
||||
@ -84,6 +86,14 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
addViewInLayout(layout, 0, aspectRatioParams);
|
||||
}
|
||||
|
||||
private void clearVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.clearVideoTextureView((TextureView) surfaceView);
|
||||
} else if (surfaceView instanceof SurfaceView) {
|
||||
player.clearVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
private void setVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.setVideoTextureView((TextureView) surfaceView);
|
||||
@ -91,9 +101,27 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
public void setSubtitleStyle(SubtitleStyle style) {
|
||||
// ensure we reset subtile style before reapplying it
|
||||
subtitleLayout.setUserDefaultStyle();
|
||||
subtitleLayout.setUserDefaultTextSize();
|
||||
|
||||
if (style.getFontSize() > 0) {
|
||||
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.getFontSize());
|
||||
}
|
||||
subtitleLayout.setPadding(style.getPaddingLeft(), style.getPaddingTop(), style.getPaddingRight(), style.getPaddingBottom());
|
||||
}
|
||||
|
||||
private void updateSurfaceView() {
|
||||
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
|
||||
View view;
|
||||
if (!useTextureView || useSecureView) {
|
||||
view = new SurfaceView(context);
|
||||
if (useSecureView) {
|
||||
((SurfaceView)view).setSecure(true);
|
||||
}
|
||||
} else {
|
||||
view = new TextureView(context);
|
||||
}
|
||||
view.setLayoutParams(layoutParams);
|
||||
|
||||
surfaceView = view;
|
||||
@ -112,29 +140,25 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
|
||||
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
|
||||
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
|
||||
* player will be called and previous
|
||||
* assignments are overridden.
|
||||
*
|
||||
* @param player The {@link SimpleExoPlayer} to use.
|
||||
* @param player The {@link ExoPlayer} to use.
|
||||
*/
|
||||
public void setPlayer(SimpleExoPlayer player) {
|
||||
public void setPlayer(ExoPlayer player) {
|
||||
if (this.player == player) {
|
||||
return;
|
||||
}
|
||||
if (this.player != null) {
|
||||
this.player.setTextOutput(null);
|
||||
this.player.setVideoListener(null);
|
||||
this.player.removeListener(componentListener);
|
||||
this.player.setVideoSurface(null);
|
||||
clearVideoView();
|
||||
}
|
||||
this.player = player;
|
||||
shutterView.setVisibility(VISIBLE);
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
if (player != null) {
|
||||
setVideoView();
|
||||
player.setVideoListener(componentListener);
|
||||
player.addListener(componentListener);
|
||||
player.setTextOutput(componentListener);
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,6 +192,13 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void useSecureView(boolean useSecureView) {
|
||||
if (useSecureView != this.useSecureView) {
|
||||
this.useSecureView = useSecureView;
|
||||
updateSurfaceView();
|
||||
}
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
this.hideShutterView = hideShutterView;
|
||||
updateShutterViewVisibility();
|
||||
@ -196,25 +227,29 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
// Video disabled so the shutter must be closed.
|
||||
shutterView.setVisibility(VISIBLE);
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
|
||||
TextRenderer.Output, ExoPlayer.EventListener {
|
||||
public void invalidateAspectRatio() {
|
||||
// Resetting aspect ratio will force layout refresh on next video size changed
|
||||
layout.invalidateAspectRatio();
|
||||
}
|
||||
|
||||
private final class ComponentListener implements Player.Listener {
|
||||
|
||||
// TextRenderer.Output implementation
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
subtitleLayout.onCues(cues);
|
||||
subtitleLayout.setCues(cues);
|
||||
}
|
||||
|
||||
// SimpleExoPlayer.VideoListener implementation
|
||||
// ExoPlayer.VideoListener implementation
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
||||
boolean isInitialRatio = layout.getAspectRatio() == 0;
|
||||
layout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height);
|
||||
layout.setAspectRatio(videoSize.height == 0 ? 1 : (videoSize.width * videoSize.pixelWidthHeightRatio) / videoSize.height);
|
||||
|
||||
// React native workaround for measuring and layout on initial load.
|
||||
if (isInitialRatio) {
|
||||
@ -230,32 +265,37 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
// ExoPlayer.EventListener implementation
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
public void onIsLoadingChanged(boolean isLoading) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
public void onPlayerError(PlaybackException e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||
public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(Tracks tracks) {
|
||||
updateForCurrentTrackSelections();
|
||||
}
|
||||
|
||||
@ -264,11 +304,6 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
// Do nothing.
|
@ -0,0 +1,80 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
|
||||
public class FullScreenPlayerView extends Dialog {
|
||||
private final PlayerControlView playerControlView;
|
||||
private final ExoPlayerView exoPlayerView;
|
||||
private ViewGroup parent;
|
||||
private final FrameLayout containerView;
|
||||
private final OnBackPressedCallback onBackPressedCallback;
|
||||
|
||||
public FullScreenPlayerView(Context context, ExoPlayerView exoPlayerView, PlayerControlView playerControlView, OnBackPressedCallback onBackPressedCallback) {
|
||||
super(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
|
||||
this.playerControlView = playerControlView;
|
||||
this.exoPlayerView = exoPlayerView;
|
||||
this.onBackPressedCallback = onBackPressedCallback;
|
||||
containerView = new FrameLayout(context);
|
||||
setContentView(containerView, generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
onBackPressedCallback.handleOnBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
parent = (FrameLayout)(exoPlayerView.getParent());
|
||||
|
||||
parent.removeView(exoPlayerView);
|
||||
containerView.addView(exoPlayerView, generateDefaultLayoutParams());
|
||||
|
||||
if (playerControlView != null) {
|
||||
ImageButton imageButton = playerControlView.findViewById(com.brentvatne.react.R.id.exo_fullscreen);
|
||||
imageButton.setImageResource(com.google.android.exoplayer2.ui.R.drawable.exo_icon_fullscreen_exit);
|
||||
imageButton.setContentDescription(getContext().getString(com.google.android.exoplayer2.ui.R.string.exo_controls_fullscreen_exit_description));
|
||||
parent.removeView(playerControlView);
|
||||
containerView.addView(playerControlView, generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
containerView.removeView(exoPlayerView);
|
||||
parent.addView(exoPlayerView, generateDefaultLayoutParams());
|
||||
|
||||
if (playerControlView != null) {
|
||||
ImageButton imageButton = playerControlView.findViewById(com.brentvatne.react.R.id.exo_fullscreen);
|
||||
imageButton.setImageResource(com.google.android.exoplayer2.ui.R.drawable.exo_icon_fullscreen_enter);
|
||||
imageButton.setContentDescription(getContext().getString(com.google.android.exoplayer2.ui.R.string.exo_controls_fullscreen_enter_description));
|
||||
containerView.removeView(playerControlView);
|
||||
parent.addView(playerControlView, generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
parent.requestLayout();
|
||||
parent = null;
|
||||
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private FrameLayout.LayoutParams generateDefaultLayoutParams() {
|
||||
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layoutParams.setMargins(0, 0, 0, 0);
|
||||
return layoutParams;
|
||||
}
|
||||
}
|
@ -9,5 +9,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
public interface ReactExoplayerConfig {
|
||||
LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount);
|
||||
|
||||
void setDisableDisconnectError(boolean disableDisconnectError);
|
||||
boolean getDisableDisconnectError();
|
||||
|
||||
DefaultBandwidthMeter getBandwidthMeter();
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
||||
public final class ReactExoplayerLoadErrorHandlingPolicy extends DefaultLoadErrorHandlingPolicy {
|
||||
private int minLoadRetryCount = Integer.MAX_VALUE;
|
||||
|
||||
public ReactExoplayerLoadErrorHandlingPolicy(int minLoadRetryCount) {
|
||||
super(minLoadRetryCount);
|
||||
this.minLoadRetryCount = minLoadRetryCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
|
||||
if (
|
||||
loadErrorInfo.exception instanceof HttpDataSourceException &&
|
||||
(loadErrorInfo.exception.getMessage() == "Unable to connect" || loadErrorInfo.exception.getMessage() == "Software caused connection abort")
|
||||
) {
|
||||
// Capture the error we get when there is no network connectivity and keep retrying it
|
||||
return 1000; // Retry every second
|
||||
} else if(loadErrorInfo.errorCount < this.minLoadRetryCount) {
|
||||
return Math.min((loadErrorInfo.errorCount - 1) * 1000, 5000); // Default timeout handling
|
||||
} else {
|
||||
return C.TIME_UNSET; // Done retrying and will return the error immediately
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimumLoadableRetryCount(int dataType) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
@ -3,19 +3,25 @@ package com.brentvatne.exoplayer;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.ViewGroupManager;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -26,6 +32,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_SRC = "src";
|
||||
private static final String PROP_SRC_URI = "uri";
|
||||
private static final String PROP_SRC_TYPE = "type";
|
||||
private static final String PROP_DRM = "drm";
|
||||
private static final String PROP_DRM_TYPE = "type";
|
||||
private static final String PROP_DRM_LICENSESERVER = "licenseServer";
|
||||
private static final String PROP_DRM_HEADERS = "headers";
|
||||
private static final String PROP_SRC_HEADERS = "requestHeaders";
|
||||
private static final String PROP_RESIZE_MODE = "resizeMode";
|
||||
private static final String PROP_REPEAT = "repeat";
|
||||
@ -39,11 +49,16 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_PAUSED = "paused";
|
||||
private static final String PROP_MUTED = "muted";
|
||||
private static final String PROP_VOLUME = "volume";
|
||||
private static final String PROP_BACK_BUFFER_DURATION_MS = "backBufferDurationMs";
|
||||
private static final String PROP_BUFFER_CONFIG = "bufferConfig";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs";
|
||||
private static final String PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs";
|
||||
private static final String PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent";
|
||||
private static final String PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent";
|
||||
private static final String PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK = "preventsDisplaySleepDuringVideoPlayback";
|
||||
private static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
||||
private static final String PROP_REPORT_BANDWIDTH = "reportBandwidth";
|
||||
private static final String PROP_SEEK = "seek";
|
||||
@ -51,15 +66,22 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount";
|
||||
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
|
||||
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||
private static final String PROP_CONTENT_START_TIME = "contentStartTime";
|
||||
private static final String PROP_DISABLE_FOCUS = "disableFocus";
|
||||
private static final String PROP_DISABLE_BUFFERING = "disableBuffering";
|
||||
private static final String PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError";
|
||||
private static final String PROP_FOCUSABLE = "focusable";
|
||||
private static final String PROP_FULLSCREEN = "fullscreen";
|
||||
private static final String PROP_USE_TEXTURE_VIEW = "useTextureView";
|
||||
private static final String PROP_SECURE_VIEW = "useSecureView";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_TYPE = "type";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_VALUE = "value";
|
||||
private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView";
|
||||
private static final String PROP_CONTROLS = "controls";
|
||||
|
||||
private static final String PROP_SUBTITLE_STYLE = "subtitleStyle";
|
||||
|
||||
private ReactExoplayerConfig config;
|
||||
|
||||
public ReactExoplayerViewManager(ReactExoplayerConfig config) {
|
||||
@ -100,6 +122,31 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DRM)
|
||||
public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) {
|
||||
if (drm != null && drm.hasKey(PROP_DRM_TYPE)) {
|
||||
String drmType = drm.hasKey(PROP_DRM_TYPE) ? drm.getString(PROP_DRM_TYPE) : null;
|
||||
String drmLicenseServer = drm.hasKey(PROP_DRM_LICENSESERVER) ? drm.getString(PROP_DRM_LICENSESERVER) : null;
|
||||
ReadableMap drmHeaders = drm.hasKey(PROP_DRM_HEADERS) ? drm.getMap(PROP_DRM_HEADERS) : null;
|
||||
if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) {
|
||||
UUID drmUUID = Util.getDrmUuid(drmType);
|
||||
videoView.setDrmType(drmUUID);
|
||||
videoView.setDrmLicenseUrl(drmLicenseServer);
|
||||
if (drmHeaders != null) {
|
||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
||||
ReadableMapKeySetIterator itr = drmHeaders.keySetIterator();
|
||||
while (itr.hasNextKey()) {
|
||||
String key = itr.nextKey();
|
||||
drmKeyRequestPropertiesList.add(key);
|
||||
drmKeyRequestPropertiesList.add(drmHeaders.getString(key));
|
||||
}
|
||||
videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0]));
|
||||
}
|
||||
videoView.setUseTextureView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SRC)
|
||||
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
|
||||
Context context = videoView.getContext().getApplicationContext();
|
||||
@ -107,8 +154,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
|
||||
Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null;
|
||||
|
||||
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
videoView.clearSrc();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,6 +183,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
if (srcUri != null) {
|
||||
videoView.setRawSrc(srcUri, extension);
|
||||
}
|
||||
} else {
|
||||
videoView.clearSrc();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -150,6 +199,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setRepeatModifier(repeat);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK, defaultBoolean = false)
|
||||
public void setPreventsDisplaySleepDuringVideoPlayback(final ReactExoplayerView videoView, final boolean preventsSleep) {
|
||||
videoView.setPreventsDisplaySleepDuringVideoPlayback(preventsSleep);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SELECTED_VIDEO_TRACK)
|
||||
public void setSelectedVideoTrack(final ReactExoplayerView videoView,
|
||||
@Nullable ReadableMap selectedVideoTrack) {
|
||||
@ -253,6 +307,31 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setDisableFocus(disableFocus);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_FOCUSABLE, defaultBoolean = true)
|
||||
public void setFocusable(final ReactExoplayerView videoView, final boolean focusable) {
|
||||
videoView.setFocusable(focusable);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BACK_BUFFER_DURATION_MS, defaultInt = 0)
|
||||
public void setBackBufferDurationMs(final ReactExoplayerView videoView, final int backBufferDurationMs) {
|
||||
videoView.setBackBufferDurationMs(backBufferDurationMs);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTENT_START_TIME, defaultInt = 0)
|
||||
public void setContentStartTime(final ReactExoplayerView videoView, final int contentStartTime) {
|
||||
videoView.setContentStartTime(contentStartTime);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DISABLE_BUFFERING, defaultBoolean = false)
|
||||
public void setDisableBuffering(final ReactExoplayerView videoView, final boolean disableBuffering) {
|
||||
videoView.setDisableBuffering(disableBuffering);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DISABLE_DISCONNECT_ERROR, defaultBoolean = false)
|
||||
public void setDisableDisconnectError(final ReactExoplayerView videoView, final boolean disableDisconnectError) {
|
||||
videoView.setDisableDisconnectError(disableDisconnectError);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
|
||||
public void setFullscreen(final ReactExoplayerView videoView, final boolean fullscreen) {
|
||||
videoView.setFullscreen(fullscreen);
|
||||
@ -263,6 +342,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setUseTextureView(useTextureView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SECURE_VIEW, defaultBoolean = true)
|
||||
public void useSecureView(final ReactExoplayerView videoView, final boolean useSecureView) {
|
||||
videoView.useSecureView(useSecureView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
|
||||
public void setHideShutterView(final ReactExoplayerView videoView, final boolean hideShutterView) {
|
||||
videoView.setHideShutterView(hideShutterView);
|
||||
@ -273,12 +357,21 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setControls(controls);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SUBTITLE_STYLE)
|
||||
public void setSubtitleStyle(final ReactExoplayerView videoView, @Nullable final ReadableMap src) {
|
||||
videoView.setSubtitleStyle(SubtitleStyle.parse(src));
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
||||
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
|
||||
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
||||
int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
|
||||
int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
|
||||
int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||
double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
||||
double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE;
|
||||
double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
||||
|
||||
if (bufferConfig != null) {
|
||||
minBufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_MIN_BUFFER_MS) : minBufferMs;
|
||||
@ -288,16 +381,23 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS) : bufferForPlaybackMs;
|
||||
bufferForPlaybackAfterRebufferMs = bufferConfig.hasKey(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
|
||||
? bufferConfig.getInt(PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) : bufferForPlaybackAfterRebufferMs;
|
||||
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
|
||||
maxHeapAllocationPercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT) : maxHeapAllocationPercent;
|
||||
minBackBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT) : minBackBufferMemoryReservePercent;
|
||||
minBufferMemoryReservePercent = bufferConfig.hasKey(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT)
|
||||
? bufferConfig.getDouble(PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT) : minBufferMemoryReservePercent;
|
||||
videoView.setBufferConfig(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, maxHeapAllocationPercent, minBackBufferMemoryReservePercent, minBufferMemoryReservePercent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startsWithValidScheme(String uriString) {
|
||||
return uriString.startsWith("http://")
|
||||
|| uriString.startsWith("https://")
|
||||
|| uriString.startsWith("content://")
|
||||
|| uriString.startsWith("file://")
|
||||
|| uriString.startsWith("asset://");
|
||||
String lowerCaseUri = uriString.toLowerCase();
|
||||
return lowerCaseUri.startsWith("http://")
|
||||
|| lowerCaseUri.startsWith("https://")
|
||||
|| lowerCaseUri.startsWith("content://")
|
||||
|| lowerCaseUri.startsWith("file://")
|
||||
|| lowerCaseUri.startsWith("asset://");
|
||||
}
|
||||
|
||||
private @ResizeMode.Mode int convertToIntDef(String resizeModeOrdinalString) {
|
@ -0,0 +1,38 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
import com.brentvatne.ReactBridgeUtils;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
/**
|
||||
* Helper file to parse SubtitleStyle prop and build a dedicated class
|
||||
*/
|
||||
public class SubtitleStyle {
|
||||
private static final String PROP_FONT_SIZE_TRACK = "fontSize";
|
||||
private static final String PROP_PADDING_BOTTOM = "paddingBottom";
|
||||
private static final String PROP_PADDING_TOP = "paddingTop";
|
||||
private static final String PROP_PADDING_LEFT = "paddingLeft";
|
||||
private static final String PROP_PADDING_RIGHT = "paddingRight";
|
||||
|
||||
private int fontSize = -1;
|
||||
private int paddingLeft = 0;
|
||||
private int paddingRight = 0;
|
||||
private int paddingTop = 0;
|
||||
private int paddingBottom = 0;
|
||||
|
||||
private SubtitleStyle() {}
|
||||
|
||||
int getFontSize() {return fontSize;}
|
||||
int getPaddingBottom() {return paddingBottom;}
|
||||
int getPaddingTop() {return paddingTop;}
|
||||
int getPaddingLeft() {return paddingLeft;}
|
||||
int getPaddingRight() {return paddingRight;}
|
||||
|
||||
public static SubtitleStyle parse(ReadableMap src) {
|
||||
SubtitleStyle subtitleStyle = new SubtitleStyle();
|
||||
subtitleStyle.fontSize = ReactBridgeUtils.safeGetInt(src, PROP_FONT_SIZE_TRACK, -1);
|
||||
subtitleStyle.paddingBottom = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_BOTTOM, 0);
|
||||
subtitleStyle.paddingTop = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_TOP, 0);
|
||||
subtitleStyle.paddingLeft = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_LEFT, 0);
|
||||
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0);
|
||||
return subtitleStyle;
|
||||
}
|
||||
}
|
@ -9,11 +9,14 @@ import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.io.StringWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
class VideoEventEmitter {
|
||||
|
||||
@ -41,6 +44,7 @@ class VideoEventEmitter {
|
||||
private static final String EVENT_RESUME = "onPlaybackResume";
|
||||
private static final String EVENT_READY = "onReadyForDisplay";
|
||||
private static final String EVENT_BUFFER = "onVideoBuffer";
|
||||
private static final String EVENT_PLAYBACK_STATE_CHANGED = "onVideoPlaybackStateChanged";
|
||||
private static final String EVENT_IDLE = "onVideoIdle";
|
||||
private static final String EVENT_TIMED_METADATA = "onTimedMetadata";
|
||||
private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy";
|
||||
@ -62,6 +66,7 @@ class VideoEventEmitter {
|
||||
EVENT_RESUME,
|
||||
EVENT_READY,
|
||||
EVENT_BUFFER,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IDLE,
|
||||
EVENT_TIMED_METADATA,
|
||||
EVENT_AUDIO_BECOMING_NOISY,
|
||||
@ -86,6 +91,7 @@ class VideoEventEmitter {
|
||||
EVENT_RESUME,
|
||||
EVENT_READY,
|
||||
EVENT_BUFFER,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IDLE,
|
||||
EVENT_TIMED_METADATA,
|
||||
EVENT_AUDIO_BECOMING_NOISY,
|
||||
@ -103,12 +109,16 @@ class VideoEventEmitter {
|
||||
private static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
|
||||
private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
|
||||
|
||||
private static final String EVENT_PROP_BUFFER_START = "bufferStart";
|
||||
private static final String EVENT_PROP_BUFFER_END = "bufferEnd";
|
||||
private static final String EVENT_PROP_DURATION = "duration";
|
||||
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||
private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||
private static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||
private static final String EVENT_PROP_CURRENT_PLAYBACK_TIME = "currentPlaybackTime";
|
||||
private static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||
private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize";
|
||||
private static final String EVENT_PROP_TRACK_ID = "trackId";
|
||||
private static final String EVENT_PROP_WIDTH = "width";
|
||||
private static final String EVENT_PROP_HEIGHT = "height";
|
||||
private static final String EVENT_PROP_ORIENTATION = "orientation";
|
||||
@ -122,11 +132,14 @@ class VideoEventEmitter {
|
||||
private static final String EVENT_PROP_ERROR = "error";
|
||||
private static final String EVENT_PROP_ERROR_STRING = "errorString";
|
||||
private static final String EVENT_PROP_ERROR_EXCEPTION = "errorException";
|
||||
private static final String EVENT_PROP_ERROR_TRACE = "errorStackTrace";
|
||||
private static final String EVENT_PROP_ERROR_CODE = "errorCode";
|
||||
|
||||
private static final String EVENT_PROP_TIMED_METADATA = "metadata";
|
||||
|
||||
private static final String EVENT_PROP_BITRATE = "bitrate";
|
||||
private static final String EVENT_PROP_BITRATE = "bitrate";
|
||||
|
||||
private static final String EVENT_PROP_IS_PLAYING = "isPlaying";
|
||||
|
||||
void setViewId(int viewId) {
|
||||
this.viewId = viewId;
|
||||
@ -137,7 +150,7 @@ class VideoEventEmitter {
|
||||
}
|
||||
|
||||
void load(double duration, double currentPosition, int videoWidth, int videoHeight,
|
||||
WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks) {
|
||||
WritableArray audioTracks, WritableArray textTracks, WritableArray videoTracks, String trackId) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_DURATION, duration / 1000D);
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||
@ -151,7 +164,7 @@ class VideoEventEmitter {
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait");
|
||||
}
|
||||
event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize);
|
||||
|
||||
event.putString(EVENT_PROP_TRACK_ID, trackId);
|
||||
event.putArray(EVENT_PROP_VIDEO_TRACKS, videoTracks);
|
||||
event.putArray(EVENT_PROP_AUDIO_TRACKS, audioTracks);
|
||||
event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks);
|
||||
@ -168,17 +181,21 @@ class VideoEventEmitter {
|
||||
receiveEvent(EVENT_LOAD, event);
|
||||
}
|
||||
|
||||
void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration) {
|
||||
void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration, double currentPlaybackTime) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D);
|
||||
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000D);
|
||||
event.putDouble(EVENT_PROP_CURRENT_PLAYBACK_TIME, currentPlaybackTime);
|
||||
receiveEvent(EVENT_PROGRESS, event);
|
||||
}
|
||||
|
||||
void bandwidthReport(double bitRateEstimate) {
|
||||
void bandwidthReport(double bitRateEstimate, int height, int width, String id) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_BITRATE, bitRateEstimate);
|
||||
event.putInt(EVENT_PROP_WIDTH, width);
|
||||
event.putInt(EVENT_PROP_HEIGHT, height);
|
||||
event.putString(EVENT_PROP_TRACK_ID, id);
|
||||
receiveEvent(EVENT_BANDWIDTH, event);
|
||||
}
|
||||
|
||||
@ -199,6 +216,12 @@ class VideoEventEmitter {
|
||||
receiveEvent(EVENT_BUFFER, map);
|
||||
}
|
||||
|
||||
void playbackStateChanged(boolean isPlaying) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying);
|
||||
receiveEvent(EVENT_PLAYBACK_STATE_CHANGED, map);
|
||||
}
|
||||
|
||||
void idle() {
|
||||
receiveEvent(EVENT_IDLE, null);
|
||||
}
|
||||
@ -224,9 +247,25 @@ class VideoEventEmitter {
|
||||
}
|
||||
|
||||
void error(String errorString, Exception exception) {
|
||||
_error(errorString, exception, "0001");
|
||||
}
|
||||
|
||||
void error(String errorString, Exception exception, String errorCode) {
|
||||
_error(errorString, exception, errorCode);
|
||||
}
|
||||
|
||||
void _error(String errorString, Exception exception, String errorCode) {
|
||||
// Prepare stack trace
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
exception.printStackTrace(pw);
|
||||
String stackTrace = sw.toString();
|
||||
|
||||
WritableMap error = Arguments.createMap();
|
||||
error.putString(EVENT_PROP_ERROR_STRING, errorString);
|
||||
error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.getMessage());
|
||||
error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString());
|
||||
error.putString(EVENT_PROP_ERROR_CODE, errorCode);
|
||||
error.putString(EVENT_PROP_ERROR_TRACE, stackTrace);
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putMap(EVENT_PROP_ERROR, error);
|
||||
receiveEvent(EVENT_ERROR, event);
|
||||
@ -242,25 +281,38 @@ class VideoEventEmitter {
|
||||
WritableArray metadataArray = Arguments.createArray();
|
||||
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
|
||||
if (entry instanceof Id3Frame) {
|
||||
|
||||
Id3Frame frame = (Id3Frame) metadata.get(i);
|
||||
Id3Frame frame = (Id3Frame) entry;
|
||||
|
||||
String value = "";
|
||||
String value = "";
|
||||
|
||||
if (frame instanceof TextInformationFrame) {
|
||||
TextInformationFrame txxxFrame = (TextInformationFrame) frame;
|
||||
value = txxxFrame.value;
|
||||
if (frame instanceof TextInformationFrame) {
|
||||
TextInformationFrame txxxFrame = (TextInformationFrame) frame;
|
||||
value = txxxFrame.value;
|
||||
}
|
||||
|
||||
String identifier = frame.id;
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("identifier", identifier);
|
||||
map.putString("value", value);
|
||||
|
||||
metadataArray.pushMap(map);
|
||||
|
||||
} else if (entry instanceof EventMessage) {
|
||||
|
||||
EventMessage eventMessage = (EventMessage) entry;
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("identifier", eventMessage.schemeIdUri);
|
||||
map.putString("value", eventMessage.value);
|
||||
metadataArray.pushMap(map);
|
||||
|
||||
}
|
||||
|
||||
String identifier = frame.id;
|
||||
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("identifier", identifier);
|
||||
map.putString("value", value);
|
||||
|
||||
metadataArray.pushMap(map);
|
||||
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
@ -1,21 +1,33 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import android.app.Activity;
|
||||
import com.brentvatne.exoplayer.DefaultReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerConfig;
|
||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactVideoPackage implements ReactPackage {
|
||||
|
||||
private ReactExoplayerConfig config;
|
||||
|
||||
public ReactVideoPackage() {
|
||||
}
|
||||
|
||||
public ReactVideoPackage(ReactExoplayerConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
return Collections.singletonList(
|
||||
new VideoDecoderPropertiesModule(reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
// Deprecated RN 0.47
|
||||
@ -23,8 +35,12 @@ public class ReactVideoPackage implements ReactPackage {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(new ReactVideoViewManager());
|
||||
if (config == null) {
|
||||
config = new DefaultReactExoplayerConfig(reactContext);
|
||||
}
|
||||
return Collections.singletonList(new ReactExoplayerViewManager(config));
|
||||
}
|
||||
}
|
||||
|
@ -1,784 +0,0 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.graphics.Matrix;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.TimedMetaData;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.webkit.CookieManager;
|
||||
import android.widget.MediaController;
|
||||
|
||||
import com.android.vending.expansion.zipfile.APKExpansionSupport;
|
||||
import com.android.vending.expansion.zipfile.ZipResourceFile;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.yqritc.scalablevideoview.ScalableType;
|
||||
import com.yqritc.scalablevideoview.ScalableVideoView;
|
||||
import com.yqritc.scalablevideoview.ScaleManager;
|
||||
import com.yqritc.scalablevideoview.Size;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.lang.Math;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class ReactVideoView extends ScalableVideoView implements
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnErrorListener,
|
||||
MediaPlayer.OnBufferingUpdateListener,
|
||||
MediaPlayer.OnSeekCompleteListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnInfoListener,
|
||||
LifecycleEventListener,
|
||||
MediaController.MediaPlayerControl {
|
||||
|
||||
public enum Events {
|
||||
EVENT_LOAD_START("onVideoLoadStart"),
|
||||
EVENT_LOAD("onVideoLoad"),
|
||||
EVENT_ERROR("onVideoError"),
|
||||
EVENT_PROGRESS("onVideoProgress"),
|
||||
EVENT_TIMED_METADATA("onTimedMetadata"),
|
||||
EVENT_SEEK("onVideoSeek"),
|
||||
EVENT_END("onVideoEnd"),
|
||||
EVENT_STALLED("onPlaybackStalled"),
|
||||
EVENT_RESUME("onPlaybackResume"),
|
||||
EVENT_READY_FOR_DISPLAY("onReadyForDisplay"),
|
||||
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
|
||||
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
|
||||
EVENT_FULLSCREEN_WILL_DISMISS("onVideoFullscreenPlayerWillDismiss"),
|
||||
EVENT_FULLSCREEN_DID_DISMISS("onVideoFullscreenPlayerDidDismiss");
|
||||
|
||||
private final String mName;
|
||||
|
||||
Events(final String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mName;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward";
|
||||
public static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward";
|
||||
public static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse";
|
||||
public static final String EVENT_PROP_REVERSE = "canPlayReverse";
|
||||
public static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
|
||||
public static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
|
||||
|
||||
public static final String EVENT_PROP_DURATION = "duration";
|
||||
public static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||
public static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||
public static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||
public static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||
public static final String EVENT_PROP_NATURALSIZE = "naturalSize";
|
||||
public static final String EVENT_PROP_WIDTH = "width";
|
||||
public static final String EVENT_PROP_HEIGHT = "height";
|
||||
public static final String EVENT_PROP_ORIENTATION = "orientation";
|
||||
public static final String EVENT_PROP_METADATA = "metadata";
|
||||
public static final String EVENT_PROP_TARGET = "target";
|
||||
public static final String EVENT_PROP_METADATA_IDENTIFIER = "identifier";
|
||||
public static final String EVENT_PROP_METADATA_VALUE = "value";
|
||||
|
||||
public static final String EVENT_PROP_ERROR = "error";
|
||||
public static final String EVENT_PROP_WHAT = "what";
|
||||
public static final String EVENT_PROP_EXTRA = "extra";
|
||||
|
||||
private ThemedReactContext mThemedReactContext;
|
||||
private RCTEventEmitter mEventEmitter;
|
||||
|
||||
private Handler mProgressUpdateHandler = new Handler();
|
||||
private Runnable mProgressUpdateRunnable = null;
|
||||
private Handler videoControlHandler = new Handler();
|
||||
private MediaController mediaController;
|
||||
|
||||
private String mSrcUriString = null;
|
||||
private String mSrcType = "mp4";
|
||||
private ReadableMap mRequestHeaders = null;
|
||||
private boolean mSrcIsNetwork = false;
|
||||
private boolean mSrcIsAsset = false;
|
||||
private ScalableType mResizeMode = ScalableType.LEFT_TOP;
|
||||
private boolean mRepeat = false;
|
||||
private boolean mPaused = false;
|
||||
private boolean mMuted = false;
|
||||
private float mVolume = 1.0f;
|
||||
private float mStereoPan = 0.0f;
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
private float mRate = 1.0f;
|
||||
private float mActiveRate = 1.0f;
|
||||
private long mSeekTime = 0;
|
||||
private boolean mPlayInBackground = false;
|
||||
private boolean mBackgroundPaused = false;
|
||||
private boolean mIsFullscreen = false;
|
||||
|
||||
private int mMainVer = 0;
|
||||
private int mPatchVer = 0;
|
||||
|
||||
private boolean mMediaPlayerValid = false; // True if mMediaPlayer is in prepared, started, paused or completed state.
|
||||
|
||||
private int mVideoDuration = 0;
|
||||
private int mVideoBufferedDuration = 0;
|
||||
private boolean isCompleted = false;
|
||||
private boolean mUseNativeControls = false;
|
||||
|
||||
public ReactVideoView(ThemedReactContext themedReactContext) {
|
||||
super(themedReactContext);
|
||||
|
||||
mThemedReactContext = themedReactContext;
|
||||
mEventEmitter = themedReactContext.getJSModule(RCTEventEmitter.class);
|
||||
themedReactContext.addLifecycleEventListener(this);
|
||||
|
||||
initializeMediaPlayerIfNeeded();
|
||||
setSurfaceTextureListener(this);
|
||||
|
||||
mProgressUpdateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (mMediaPlayerValid && !isCompleted && !mPaused && !mBackgroundPaused) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable
|
||||
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, mVideoDuration / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_PROGRESS.toString(), event);
|
||||
|
||||
// Check for update after an interval
|
||||
mProgressUpdateHandler.postDelayed(mProgressUpdateRunnable, Math.round(mProgressUpdateInterval));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (mUseNativeControls) {
|
||||
initializeMediaControllerIfNeeded();
|
||||
mediaController.show();
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("DrawAllocation")
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
if (!changed || !mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
int videoWidth = getVideoWidth();
|
||||
int videoHeight = getVideoHeight();
|
||||
|
||||
if (videoWidth == 0 || videoHeight == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Size viewSize = new Size(getWidth(), getHeight());
|
||||
Size videoSize = new Size(videoWidth, videoHeight);
|
||||
ScaleManager scaleManager = new ScaleManager(viewSize, videoSize);
|
||||
Matrix matrix = scaleManager.getScaleMatrix(mScalableType);
|
||||
if (matrix != null) {
|
||||
setTransform(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaPlayerIfNeeded() {
|
||||
if (mMediaPlayer == null) {
|
||||
mMediaPlayerValid = false;
|
||||
mMediaPlayer = new MediaPlayer();
|
||||
mMediaPlayer.setScreenOnWhilePlaying(true);
|
||||
mMediaPlayer.setOnVideoSizeChangedListener(this);
|
||||
mMediaPlayer.setOnErrorListener(this);
|
||||
mMediaPlayer.setOnPreparedListener(this);
|
||||
mMediaPlayer.setOnBufferingUpdateListener(this);
|
||||
mMediaPlayer.setOnSeekCompleteListener(this);
|
||||
mMediaPlayer.setOnCompletionListener(this);
|
||||
mMediaPlayer.setOnInfoListener(this);
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
mMediaPlayer.setOnTimedMetaDataAvailableListener(new TimedMetaDataAvailableListener());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaControllerIfNeeded() {
|
||||
if (mediaController == null) {
|
||||
mediaController = new MediaController(this.getContext());
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupMediaPlayerResources() {
|
||||
if ( mediaController != null ) {
|
||||
mediaController.hide();
|
||||
}
|
||||
if ( mMediaPlayer != null ) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mMediaPlayer.setOnTimedMetaDataAvailableListener(null);
|
||||
}
|
||||
mMediaPlayerValid = false;
|
||||
release();
|
||||
}
|
||||
if (mIsFullscreen) {
|
||||
setFullscreen(false);
|
||||
}
|
||||
if (mThemedReactContext != null) {
|
||||
mThemedReactContext.removeLifecycleEventListener(this);
|
||||
mThemedReactContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders) {
|
||||
setSrc(uriString, type, isNetwork, isAsset, requestHeaders, 0, 0);
|
||||
}
|
||||
|
||||
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders, final int expansionMainVersion, final int expansionPatchVersion) {
|
||||
|
||||
mSrcUriString = uriString;
|
||||
mSrcType = type;
|
||||
mSrcIsNetwork = isNetwork;
|
||||
mSrcIsAsset = isAsset;
|
||||
mRequestHeaders = requestHeaders;
|
||||
mMainVer = expansionMainVersion;
|
||||
mPatchVer = expansionPatchVersion;
|
||||
|
||||
|
||||
mMediaPlayerValid = false;
|
||||
mVideoDuration = 0;
|
||||
mVideoBufferedDuration = 0;
|
||||
|
||||
initializeMediaPlayerIfNeeded();
|
||||
mMediaPlayer.reset();
|
||||
|
||||
try {
|
||||
if (isNetwork) {
|
||||
// Use the shared CookieManager to access the cookies
|
||||
// set by WebViews inside the same app
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
|
||||
Uri parsedUrl = Uri.parse(uriString);
|
||||
Uri.Builder builtUrl = parsedUrl.buildUpon();
|
||||
|
||||
String cookie = cookieManager.getCookie(builtUrl.build().toString());
|
||||
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
|
||||
if (cookie != null) {
|
||||
headers.put("Cookie", cookie);
|
||||
}
|
||||
|
||||
if (mRequestHeaders != null) {
|
||||
headers.putAll(toStringMap(mRequestHeaders));
|
||||
}
|
||||
|
||||
/* According to https://github.com/react-native-community/react-native-video/pull/537
|
||||
* there is an issue with this where it can cause a IOException.
|
||||
* TODO: diagnose this exception and fix it
|
||||
*/
|
||||
setDataSource(mThemedReactContext, parsedUrl, headers);
|
||||
} else if (isAsset) {
|
||||
if (uriString.startsWith("content://")) {
|
||||
Uri parsedUrl = Uri.parse(uriString);
|
||||
setDataSource(mThemedReactContext, parsedUrl);
|
||||
} else {
|
||||
setDataSource(uriString);
|
||||
}
|
||||
} else {
|
||||
ZipResourceFile expansionFile= null;
|
||||
AssetFileDescriptor fd= null;
|
||||
if(mMainVer>0) {
|
||||
try {
|
||||
expansionFile = APKExpansionSupport.getAPKExpansionZipFile(mThemedReactContext, mMainVer, mPatchVer);
|
||||
fd = expansionFile.getAssetFileDescriptor(uriString.replace(".mp4","") + ".mp4");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if(fd==null) {
|
||||
int identifier = mThemedReactContext.getResources().getIdentifier(
|
||||
uriString,
|
||||
"drawable",
|
||||
mThemedReactContext.getPackageName()
|
||||
);
|
||||
if (identifier == 0) {
|
||||
identifier = mThemedReactContext.getResources().getIdentifier(
|
||||
uriString,
|
||||
"raw",
|
||||
mThemedReactContext.getPackageName()
|
||||
);
|
||||
}
|
||||
setRawData(identifier);
|
||||
}
|
||||
else {
|
||||
setDataSource(fd.getFileDescriptor(), fd.getStartOffset(),fd.getLength());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap src = Arguments.createMap();
|
||||
|
||||
WritableMap wRequestHeaders = Arguments.createMap();
|
||||
wRequestHeaders.merge(mRequestHeaders);
|
||||
|
||||
src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString);
|
||||
src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type);
|
||||
src.putMap(ReactVideoViewManager.PROP_SRC_HEADERS, wRequestHeaders);
|
||||
src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork);
|
||||
if(mMainVer>0) {
|
||||
src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer);
|
||||
if(mPatchVer>0) {
|
||||
src.putInt(ReactVideoViewManager.PROP_SRC_PATCHVER, mPatchVer);
|
||||
}
|
||||
}
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putMap(ReactVideoViewManager.PROP_SRC, src);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD_START.toString(), event);
|
||||
isCompleted = false;
|
||||
|
||||
try {
|
||||
prepareAsync(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void setResizeModeModifier(final ScalableType resizeMode) {
|
||||
mResizeMode = resizeMode;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
setScalableType(resizeMode);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setRepeatModifier(final boolean repeat) {
|
||||
|
||||
mRepeat = repeat;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
setLooping(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPausedModifier(final boolean paused) {
|
||||
mPaused = paused;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPaused) {
|
||||
if (mMediaPlayer.isPlaying()) {
|
||||
pause();
|
||||
}
|
||||
} else {
|
||||
if (!mMediaPlayer.isPlaying()) {
|
||||
start();
|
||||
// Setting the rate unpauses, so we have to wait for an unpause
|
||||
if (mRate != mActiveRate) {
|
||||
setRateModifier(mRate);
|
||||
}
|
||||
|
||||
// Also Start the Progress Update Handler
|
||||
mProgressUpdateHandler.post(mProgressUpdateRunnable);
|
||||
}
|
||||
}
|
||||
setKeepScreenOn(!mPaused);
|
||||
}
|
||||
|
||||
// reduces the volume based on stereoPan
|
||||
private float calulateRelativeVolume() {
|
||||
float relativeVolume = (mVolume * (1 - Math.abs(mStereoPan)));
|
||||
// only one decimal allowed
|
||||
BigDecimal roundRelativeVolume = new BigDecimal(relativeVolume).setScale(1, BigDecimal.ROUND_HALF_UP);
|
||||
return roundRelativeVolume.floatValue();
|
||||
}
|
||||
|
||||
public void setMutedModifier(final boolean muted) {
|
||||
mMuted = muted;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMuted) {
|
||||
setVolume(0, 0);
|
||||
} else if (mStereoPan < 0) {
|
||||
// louder on the left channel
|
||||
setVolume(mVolume, calulateRelativeVolume());
|
||||
} else if (mStereoPan > 0) {
|
||||
// louder on the right channel
|
||||
setVolume(calulateRelativeVolume(), mVolume);
|
||||
} else {
|
||||
// same volume on both channels
|
||||
setVolume(mVolume, mVolume);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVolumeModifier(final float volume) {
|
||||
mVolume = volume;
|
||||
setMutedModifier(mMuted);
|
||||
}
|
||||
|
||||
public void setStereoPan(final float stereoPan) {
|
||||
mStereoPan = stereoPan;
|
||||
setMutedModifier(mMuted);
|
||||
}
|
||||
|
||||
public void setProgressUpdateInterval(final float progressUpdateInterval) {
|
||||
mProgressUpdateInterval = progressUpdateInterval;
|
||||
}
|
||||
|
||||
public void setRateModifier(final float rate) {
|
||||
mRate = rate;
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!mPaused) { // Applying the rate while paused will cause the video to start
|
||||
/* Per https://stackoverflow.com/questions/39442522/setplaybackparams-causes-illegalstateexception
|
||||
* Some devices throw an IllegalStateException if you set the rate without first calling reset()
|
||||
* TODO: Call reset() then reinitialize the player
|
||||
*/
|
||||
try {
|
||||
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
|
||||
mActiveRate = rate;
|
||||
} catch (Exception e) {
|
||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Unable to set rate, unsupported on this device");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setFullscreen(boolean isFullscreen) {
|
||||
if (isFullscreen == mIsFullscreen) {
|
||||
return; // Avoid generating events when nothing is changing
|
||||
}
|
||||
mIsFullscreen = isFullscreen;
|
||||
|
||||
Activity activity = mThemedReactContext.getCurrentActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
Window window = activity.getWindow();
|
||||
View decorView = window.getDecorView();
|
||||
int uiOptions;
|
||||
if (mIsFullscreen) {
|
||||
if (Build.VERSION.SDK_INT >= 19) { // 4.4+
|
||||
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
} else {
|
||||
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_WILL_PRESENT.toString(), null);
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_DID_PRESENT.toString(), null);
|
||||
} else {
|
||||
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_WILL_DISMISS.toString(), null);
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_FULLSCREEN_DID_DISMISS.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public void applyModifiers() {
|
||||
setResizeModeModifier(mResizeMode);
|
||||
setRepeatModifier(mRepeat);
|
||||
setPausedModifier(mPaused);
|
||||
setMutedModifier(mMuted);
|
||||
setProgressUpdateInterval(mProgressUpdateInterval);
|
||||
setRateModifier(mRate);
|
||||
}
|
||||
|
||||
public void setPlayInBackground(final boolean playInBackground) {
|
||||
|
||||
mPlayInBackground = playInBackground;
|
||||
}
|
||||
|
||||
public void setControls(boolean controls) {
|
||||
this.mUseNativeControls = controls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPlayer mp) {
|
||||
|
||||
mMediaPlayerValid = true;
|
||||
mVideoDuration = mp.getDuration();
|
||||
|
||||
WritableMap naturalSize = Arguments.createMap();
|
||||
naturalSize.putInt(EVENT_PROP_WIDTH, mp.getVideoWidth());
|
||||
naturalSize.putInt(EVENT_PROP_HEIGHT, mp.getVideoHeight());
|
||||
if (mp.getVideoWidth() > mp.getVideoHeight())
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape");
|
||||
else
|
||||
naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait");
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_DURATION, mVideoDuration / 1000.0);
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, mp.getCurrentPosition() / 1000.0);
|
||||
event.putMap(EVENT_PROP_NATURALSIZE, naturalSize);
|
||||
// TODO: Actually check if you can.
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_SLOW_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_REVERSE, true);
|
||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_BACKWARD, true);
|
||||
event.putBoolean(EVENT_PROP_STEP_FORWARD, true);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD.toString(), event);
|
||||
|
||||
applyModifiers();
|
||||
|
||||
if (mUseNativeControls) {
|
||||
initializeMediaControllerIfNeeded();
|
||||
mediaController.setMediaPlayer(this);
|
||||
mediaController.setAnchorView(this);
|
||||
|
||||
videoControlHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mediaController.setEnabled(true);
|
||||
mediaController.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectTimedMetadataTrack(mp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(MediaPlayer mp, int what, int extra) {
|
||||
|
||||
WritableMap error = Arguments.createMap();
|
||||
error.putInt(EVENT_PROP_WHAT, what);
|
||||
error.putInt(EVENT_PROP_EXTRA, extra);
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putMap(EVENT_PROP_ERROR, error);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_ERROR.toString(), event);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInfo(MediaPlayer mp, int what, int extra) {
|
||||
switch (what) {
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_STALLED.toString(), Arguments.createMap());
|
||||
break;
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_RESUME.toString(), Arguments.createMap());
|
||||
break;
|
||||
case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_READY_FOR_DISPLAY.toString(), Arguments.createMap());
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBufferingUpdate(MediaPlayer mp, int percent) {
|
||||
selectTimedMetadataTrack(mp);
|
||||
mVideoBufferedDuration = (int) Math.round((double) (mVideoDuration * percent) / 100.0);
|
||||
}
|
||||
|
||||
public void onSeekComplete(MediaPlayer mp) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_SEEK_TIME, mSeekTime / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_SEEK.toString(), event);
|
||||
mSeekTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int msec) {
|
||||
if (mMediaPlayerValid) {
|
||||
mSeekTime = msec;
|
||||
super.seekTo(msec);
|
||||
if (isCompleted && mVideoDuration != 0 && msec < mVideoDuration) {
|
||||
isCompleted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBufferPercentage() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPause() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeekBackward() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeekForward() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioSessionId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
isCompleted = true;
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_END.toString(), null);
|
||||
if (!mRepeat) {
|
||||
setKeepScreenOn(false);
|
||||
}
|
||||
}
|
||||
|
||||
// This is not fully tested and does not work for all forms of timed metadata
|
||||
@TargetApi(23) // 6.0
|
||||
public class TimedMetaDataAvailableListener
|
||||
implements MediaPlayer.OnTimedMetaDataAvailableListener
|
||||
{
|
||||
public void onTimedMetaDataAvailable(MediaPlayer mp, TimedMetaData data) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
|
||||
try {
|
||||
String rawMeta = new String(data.getMetaData(), "UTF-8");
|
||||
WritableMap id3 = Arguments.createMap();
|
||||
|
||||
id3.putString(EVENT_PROP_METADATA_VALUE, rawMeta.substring(rawMeta.lastIndexOf("\u0003") + 1));
|
||||
id3.putString(EVENT_PROP_METADATA_IDENTIFIER, "id3/TDEN");
|
||||
|
||||
WritableArray metadata = new WritableNativeArray();
|
||||
|
||||
metadata.pushMap(id3);
|
||||
|
||||
event.putArray(EVENT_PROP_METADATA, metadata);
|
||||
event.putDouble(EVENT_PROP_TARGET, getId());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_TIMED_METADATA.toString(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
mMediaPlayerValid = false;
|
||||
super.onDetachedFromWindow();
|
||||
setKeepScreenOn(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if(mMainVer>0) {
|
||||
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders, mMainVer, mPatchVer);
|
||||
}
|
||||
else {
|
||||
setSrc(mSrcUriString, mSrcType, mSrcIsNetwork, mSrcIsAsset, mRequestHeaders);
|
||||
}
|
||||
setKeepScreenOn(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
if (mMediaPlayerValid && !mPaused && !mPlayInBackground) {
|
||||
/* Pause the video in background
|
||||
* Don't update the paused prop, developers should be able to update it on background
|
||||
* so that when you return to the app the video is paused
|
||||
*/
|
||||
mBackgroundPaused = true;
|
||||
mMediaPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
mBackgroundPaused = false;
|
||||
if (mMediaPlayerValid && !mPlayInBackground && !mPaused) {
|
||||
new Handler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Restore original state
|
||||
setPausedModifier(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* toStringMap converts a {@link ReadableMap} into a HashMap.
|
||||
*
|
||||
* @param readableMap The ReadableMap to be conveted.
|
||||
* @return A HashMap containing the data that was in the ReadableMap.
|
||||
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
|
||||
*/
|
||||
public static Map<String, String> toStringMap(@Nullable ReadableMap readableMap) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (readableMap == null)
|
||||
return result;
|
||||
|
||||
com.facebook.react.bridge.ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
|
||||
while (iterator.hasNextKey()) {
|
||||
String key = iterator.nextKey();
|
||||
result.put(key, readableMap.getString(key));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Select track (so we can use it to listen to timed meta data updates)
|
||||
private void selectTimedMetadataTrack(MediaPlayer mp) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return;
|
||||
}
|
||||
try { // It's possible this could throw an exception if the framework doesn't support getting track info
|
||||
MediaPlayer.TrackInfo[] trackInfo = mp.getTrackInfo();
|
||||
for (int i = 0; i < trackInfo.length; ++i) {
|
||||
if (trackInfo[i].getTrackType() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
|
||||
mp.selectTrack(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import com.brentvatne.react.ReactVideoView.Events;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.SimpleViewManager;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.yqritc.scalablevideoview.ScalableType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
|
||||
|
||||
public static final String REACT_CLASS = "RCTVideo";
|
||||
|
||||
public static final String PROP_SRC = "src";
|
||||
public static final String PROP_SRC_URI = "uri";
|
||||
public static final String PROP_SRC_TYPE = "type";
|
||||
public static final String PROP_SRC_HEADERS = "requestHeaders";
|
||||
public static final String PROP_SRC_IS_NETWORK = "isNetwork";
|
||||
public static final String PROP_SRC_MAINVER = "mainVer";
|
||||
public static final String PROP_SRC_PATCHVER = "patchVer";
|
||||
public static final String PROP_SRC_IS_ASSET = "isAsset";
|
||||
public static final String PROP_RESIZE_MODE = "resizeMode";
|
||||
public static final String PROP_REPEAT = "repeat";
|
||||
public static final String PROP_PAUSED = "paused";
|
||||
public static final String PROP_MUTED = "muted";
|
||||
public static final String PROP_VOLUME = "volume";
|
||||
public static final String PROP_STEREO_PAN = "stereoPan";
|
||||
public static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
||||
public static final String PROP_SEEK = "seek";
|
||||
public static final String PROP_RATE = "rate";
|
||||
public static final String PROP_FULLSCREEN = "fullscreen";
|
||||
public static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||
public static final String PROP_CONTROLS = "controls";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactVideoView createViewInstance(ThemedReactContext themedReactContext) {
|
||||
return new ReactVideoView(themedReactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropViewInstance(ReactVideoView view) {
|
||||
super.onDropViewInstance(view);
|
||||
view.cleanupMediaPlayerResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map getExportedCustomDirectEventTypeConstants() {
|
||||
MapBuilder.Builder builder = MapBuilder.builder();
|
||||
for (Events event : Events.values()) {
|
||||
builder.put(event.toString(), MapBuilder.of("registrationName", event.toString()));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Map getExportedViewConstants() {
|
||||
return MapBuilder.of(
|
||||
"ScaleNone", Integer.toString(ScalableType.LEFT_TOP.ordinal()),
|
||||
"ScaleToFill", Integer.toString(ScalableType.FIT_XY.ordinal()),
|
||||
"ScaleAspectFit", Integer.toString(ScalableType.FIT_CENTER.ordinal()),
|
||||
"ScaleAspectFill", Integer.toString(ScalableType.CENTER_CROP.ordinal())
|
||||
);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SRC)
|
||||
public void setSrc(final ReactVideoView videoView, @Nullable ReadableMap src) {
|
||||
int mainVer = src.getInt(PROP_SRC_MAINVER);
|
||||
int patchVer = src.getInt(PROP_SRC_PATCHVER);
|
||||
if(mainVer<0) { mainVer = 0; }
|
||||
if(patchVer<0) { patchVer = 0; }
|
||||
if(mainVer>0) {
|
||||
videoView.setSrc(
|
||||
src.getString(PROP_SRC_URI),
|
||||
src.getString(PROP_SRC_TYPE),
|
||||
src.getBoolean(PROP_SRC_IS_NETWORK),
|
||||
src.getBoolean(PROP_SRC_IS_ASSET),
|
||||
src.getMap(PROP_SRC_HEADERS),
|
||||
mainVer,
|
||||
patchVer
|
||||
);
|
||||
}
|
||||
else {
|
||||
videoView.setSrc(
|
||||
src.getString(PROP_SRC_URI),
|
||||
src.getString(PROP_SRC_TYPE),
|
||||
src.getBoolean(PROP_SRC_IS_NETWORK),
|
||||
src.getBoolean(PROP_SRC_IS_ASSET),
|
||||
src.getMap(PROP_SRC_HEADERS)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RESIZE_MODE)
|
||||
public void setResizeMode(final ReactVideoView videoView, final String resizeModeOrdinalString) {
|
||||
videoView.setResizeModeModifier(ScalableType.values()[Integer.parseInt(resizeModeOrdinalString)]);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_REPEAT, defaultBoolean = false)
|
||||
public void setRepeat(final ReactVideoView videoView, final boolean repeat) {
|
||||
videoView.setRepeatModifier(repeat);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||
public void setPaused(final ReactVideoView videoView, final boolean paused) {
|
||||
videoView.setPausedModifier(paused);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MUTED, defaultBoolean = false)
|
||||
public void setMuted(final ReactVideoView videoView, final boolean muted) {
|
||||
videoView.setMutedModifier(muted);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
|
||||
public void setVolume(final ReactVideoView videoView, final float volume) {
|
||||
videoView.setVolumeModifier(volume);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_STEREO_PAN)
|
||||
public void setStereoPan(final ReactVideoView videoView, final float stereoPan) {
|
||||
videoView.setStereoPan(stereoPan);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
|
||||
public void setProgressUpdateInterval(final ReactVideoView videoView, final float progressUpdateInterval) {
|
||||
videoView.setProgressUpdateInterval(progressUpdateInterval);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SEEK)
|
||||
public void setSeek(final ReactVideoView videoView, final float seek) {
|
||||
videoView.seekTo(Math.round(seek * 1000.0f));
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RATE)
|
||||
public void setRate(final ReactVideoView videoView, final float rate) {
|
||||
videoView.setRateModifier(rate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
|
||||
public void setFullscreen(final ReactVideoView videoView, final boolean fullscreen) {
|
||||
videoView.setFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||
public void setPlayInBackground(final ReactVideoView videoView, final boolean playInBackground) {
|
||||
videoView.setPlayInBackground(playInBackground);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||
public void setControls(final ReactVideoView videoView, final boolean controls) {
|
||||
videoView.setControls(controls);
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.brentvatne.react;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaDrm;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.UnsupportedSchemeException;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
public class VideoDecoderPropertiesModule extends ReactContextBaseJavaModule {
|
||||
|
||||
ReactApplicationContext reactContext;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VideoDecoderProperties";
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@ReactMethod
|
||||
public void getWidevineLevel(Promise p) {
|
||||
int widevineLevel = 0;
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
p.resolve(widevineLevel);
|
||||
return;
|
||||
}
|
||||
final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
|
||||
final String WIDEVINE_SECURITY_LEVEL_1 = "L1";
|
||||
final String WIDEVINE_SECURITY_LEVEL_2 = "L2";
|
||||
final String WIDEVINE_SECURITY_LEVEL_3 = "L3";
|
||||
final String SECURITY_LEVEL_PROPERTY = "securityLevel";
|
||||
|
||||
String securityProperty = null;
|
||||
try {
|
||||
MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID);
|
||||
securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY);
|
||||
} catch (UnsupportedSchemeException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (securityProperty == null) {
|
||||
p.resolve(widevineLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (securityProperty) {
|
||||
case WIDEVINE_SECURITY_LEVEL_1: {
|
||||
widevineLevel = 1;
|
||||
break;
|
||||
}
|
||||
case WIDEVINE_SECURITY_LEVEL_2: {
|
||||
widevineLevel = 2;
|
||||
break;
|
||||
}
|
||||
case WIDEVINE_SECURITY_LEVEL_3: {
|
||||
widevineLevel = 3;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// widevineLevel 0
|
||||
break;
|
||||
}
|
||||
}
|
||||
p.resolve(widevineLevel);
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@ReactMethod
|
||||
public void isCodecSupported(String mimeType, int width, int height, Promise p) {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
p.resolve(false);
|
||||
return;
|
||||
}
|
||||
MediaCodecList mRegularCodecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
|
||||
String codecName = mRegularCodecs.findDecoderForFormat(format);
|
||||
if (codecName == null) {
|
||||
p.resolve(false);
|
||||
} else {
|
||||
p.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ReactMethod
|
||||
public void isHEVCSupported(Promise p) {
|
||||
isCodecSupported("video/hevc", 1920, 1080, p);
|
||||
}
|
||||
|
||||
public VideoDecoderPropertiesModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
}
|
@ -71,6 +71,14 @@
|
||||
android:paddingRight="4dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="#FFBEBEBE"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_fullscreen"
|
||||
style="@style/ExoMediaButton.FullScreen"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="4dp"
|
||||
android:scaleType="fitCenter" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -8,7 +8,12 @@
|
||||
<string name="error_querying_decoders">Unable to query device decoders</string>
|
||||
|
||||
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="unrecognized_media_format">Unrecognized media format</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
</resources>
|
7
android/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="ExoMediaButton.FullScreen">
|
||||
<item name="android:src">@drawable/exo_icon_fullscreen_enter</item>
|
||||
<item name="android:contentDescription">@string/exo_controls_fullscreen_enter_description</item>
|
||||
</style>
|
||||
</resources>
|
47
docs/DEBUGGING.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Advanced debuging and common issues
|
||||
|
||||
### HTTP playback doesn't work or Black Screen on Release build (Android)
|
||||
If your video work on Debug mode, but on Release you see only black screen, please, check the link to your video. If you use 'http' protocol there, you will need to add next string to your AndroidManifest.xml file. [Details here](https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic)
|
||||
|
||||
```
|
||||
<application
|
||||
...
|
||||
android:usesCleartextTraffic="true"
|
||||
>
|
||||
```
|
||||
|
||||
### Decoder Issue (Android)
|
||||
|
||||
Devices have a maximum of simulataneous possible playback. It means you have reach this limit. Exoplayer returns: 'Unable to instantiate decoder'
|
||||
|
||||
**known issue**: This issue happen really often in debug mode.
|
||||
|
||||
## You cannot play clean content (all OS)
|
||||
|
||||
Here are the steps to consider before opening a ticket in issue tracker
|
||||
|
||||
### Check you can access to remote file
|
||||
|
||||
Ensure you can download to manifest / content file with a browser for exemple
|
||||
|
||||
### Check another player can read the content
|
||||
|
||||
Usually clear playback can be read with all Video player. Then you should ensure content can be played without any issue with another player ([VideoLan/VLC](https://www.videolan.org/vlc/) is a good reference implementation)
|
||||
|
||||
## You cannot play protected content (all OS)
|
||||
|
||||
### Protected content gives error (token error / access forbidden)
|
||||
|
||||
If content is protected with an access token or any other http header, ensure you can access to you data with a wget call or a rest client app. You need to provide all needed access token / authentication parameters.
|
||||
|
||||
### Everything seems correct but content cannot be accessed
|
||||
|
||||
You need to record network trace to ensure communications with server is correct.
|
||||
[Charles proxy](https://www.charlesproxy.com/) is a simple and usefull tool to sniff all http/https calls.
|
||||
With this tool you should be able to analyze what is going on with network. You will see all access to content and DRM, audio / vido chuncks, ...
|
||||
|
||||
Then try to compare exchanges with previous tests you made.
|
||||
|
||||
### It's still not working
|
||||
|
||||
You can try to open a ticket now !
|
139
docs/DRM.md
Normal file
@ -0,0 +1,139 @@
|
||||
# DRM
|
||||
|
||||
## Provide DRM data (only tested with http/https assets)
|
||||
|
||||
You can provide some configuration to allow DRM playback.
|
||||
This feature will disable the use of `TextureView` on Android.
|
||||
|
||||
DRM object allows this members:
|
||||
|
||||
| Property | Type | Default | Platform | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [`type`](#type) | DRMType | undefined | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) |
|
||||
| [`licenseServer`](#licenseserver) | string | undefined | iOS/Android | Specifies the license server URL |
|
||||
| [`headers`](#headers) | Object | undefined | iOS/Android | Specifies the headers send to the license server URL on license acquisition |
|
||||
| [`contentId`](#contentid) | string | undefined | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) |
|
||||
| [`certificateUrl`](#certificateurl) | string | undefined | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file |
|
||||
| [`base64Certificate`](#base64certificate) | bool | false | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 |
|
||||
| [`getLicense`](#getlicense)| function | undefined | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream |
|
||||
|
||||
### `base64Certificate`
|
||||
|
||||
Whether or not the certificate url returns it on base64.
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
### `certificateUrl`
|
||||
|
||||
URL to fetch a valid certificate for FairPlay.
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
### `getLicense`
|
||||
|
||||
`licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` (as ASCII string, you will probably need to convert it to base 64) obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`.
|
||||
You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`.
|
||||
|
||||
With this prop you can override the license acquisition flow, as an example:
|
||||
|
||||
```js
|
||||
getLicense: (spcString) => {
|
||||
const base64spc = Base64.encode(spcString);
|
||||
const formData = new FormData();
|
||||
formData.append('spc', base64spc);
|
||||
return fetch(`https://license.pallycon.com/ri/licenseManager.do`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'pallycon-customdata-v2': 'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData
|
||||
}).then(response => response.text()).then((response) => {
|
||||
return response;
|
||||
}).catch((error) => {
|
||||
console.error('Error', error);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
### `headers`
|
||||
|
||||
You can customize headers send to the licenseServer.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
source={{
|
||||
uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
|
||||
}}
|
||||
drm={{
|
||||
type: DRMType.WIDEVINE,
|
||||
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
||||
headers: {
|
||||
'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU'
|
||||
},
|
||||
}}
|
||||
```
|
||||
|
||||
### `licenseServer`
|
||||
|
||||
The URL pointing to the licenseServer that will provide the authorization to play the protected stream.
|
||||
|
||||
### `type`
|
||||
|
||||
You can specify the DRM type, either by string or using the exported DRMType enum.
|
||||
Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY.
|
||||
for iOS: DRMType.FAIRPLAY
|
||||
|
||||
## Common Usage Scenarios
|
||||
|
||||
### Send cookies to license server
|
||||
|
||||
You can send Cookies to the license server via `headers` prop. Example:
|
||||
|
||||
```js
|
||||
drm: {
|
||||
type: DRMType.WIDEVINE
|
||||
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
||||
headers: {
|
||||
'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Custom License Acquisition (only iOS for now)
|
||||
|
||||
```js
|
||||
drm: {
|
||||
type: DRMType.FAIRPLAY,
|
||||
getLicense: (spcString) => {
|
||||
const base64spc = Base64.encode(spcString);
|
||||
return fetch('YOUR LICENSE SERVER HERE', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
getFairplayLicense: {
|
||||
foo: 'bar',
|
||||
spcMessage: base64spc,
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then((response) => {
|
||||
if (response && response.getFairplayLicenseResponse
|
||||
&& response.getFairplayLicenseResponse.ckcResponse) {
|
||||
return response.getFairplayLicenseResponse.ckcResponse;
|
||||
}
|
||||
throw new Error('No correct response');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('CKC error', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
9
docs/PROJECTS.md
Normal file
@ -0,0 +1,9 @@
|
||||
This page links other open source projects which can be usefull for your player implementation
|
||||
|
||||
# UI over react-native-video
|
||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
|
||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||
|
||||
# Other tools
|
||||
- [react-native-music-control](https://github.com/tanguyantoine/react-native-music-control): A toolbox to control player over media session
|
@ -1,9 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Vincent Riemer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
278
dom/RCTVideo.js
@ -1,278 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { RCTEvent, RCTView, type RCTBridge } from "react-native-dom";
|
||||
import shaka from "shaka-player";
|
||||
|
||||
import resizeModes from "./resizeModes";
|
||||
import type { VideoSource } from "./types";
|
||||
import RCTVideoEvent from "./RCTVideoEvent";
|
||||
|
||||
class RCTVideo extends RCTView {
|
||||
playPromise: Promise<void> = Promise.resolve();
|
||||
progressTimer: number;
|
||||
videoElement: HTMLVideoElement;
|
||||
|
||||
onEnd: boolean = false;
|
||||
onLoad: boolean = false;
|
||||
onLoadStart: boolean = false;
|
||||
onProgress: boolean = false;
|
||||
|
||||
_paused: boolean = false;
|
||||
_progressUpdateInterval: number = 250.0;
|
||||
_savedVolume: number = 1.0;
|
||||
|
||||
constructor(bridge: RCTBridge) {
|
||||
super(bridge);
|
||||
|
||||
this.eventDispatcher = bridge.getModuleByName("EventDispatcher");
|
||||
|
||||
shaka.polyfill.installAll();
|
||||
|
||||
this.onEnd = this.onEnd.bind(this);
|
||||
this.onLoad = this.onLoad.bind(this);
|
||||
this.onLoadStart = this.onLoadStart.bind(this);
|
||||
this.onPlay = this.onPlay.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
|
||||
this.videoElement = this.initializeVideoElement();
|
||||
this.videoElement.addEventListener("ended", this.onEnd);
|
||||
this.videoElement.addEventListener("loadeddata", this.onLoad);
|
||||
this.videoElement.addEventListener("canplay", this.onReadyForDisplay);
|
||||
this.videoElement.addEventListener("loadstart", this.onLoadStart);
|
||||
this.videoElement.addEventListener("pause", this.onPause);
|
||||
this.videoElement.addEventListener("play", this.onPlay);
|
||||
this.player = new shaka.Player(this.videoElement);
|
||||
|
||||
this.muted = false;
|
||||
this.rate = 1.0;
|
||||
this.volume = 1.0;
|
||||
this.childContainer.appendChild(this.videoElement);
|
||||
}
|
||||
|
||||
detachFromView(view: UIView) {
|
||||
this.videoElement.removeEventListener("ended", this.onEnd);
|
||||
this.videoElement.removeEventListener("loadeddata", this.onLoad);
|
||||
this.videoElement.removeEventListener("canplay", this.onReadyForDisplay);
|
||||
this.videoElement.removeEventListener("loadstart", this.onLoadStart);
|
||||
this.videoElement.removeEventListener("pause", this.onPause);
|
||||
this.videoElement.removeEventListener("play", this.onPlay);
|
||||
|
||||
this.stopProgressTimer();
|
||||
}
|
||||
|
||||
initializeVideoElement() {
|
||||
const elem = document.createElement("video");
|
||||
|
||||
Object.assign(elem.style, {
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
});
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
presentFullscreenPlayer() {
|
||||
this.videoElement.webkitRequestFullScreen();
|
||||
}
|
||||
|
||||
set controls(value: boolean) {
|
||||
this.videoElement.controls = value;
|
||||
this.videoElement.style.pointerEvents = value ? "auto" : "";
|
||||
}
|
||||
|
||||
set id(value: string) {
|
||||
this.videoElement.id = value;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
this.videoElement.muted = true;
|
||||
}
|
||||
|
||||
set paused(value: boolean) {
|
||||
if (value) {
|
||||
this.videoElement.pause();
|
||||
} else {
|
||||
this.requestPlay();
|
||||
}
|
||||
this._paused = value;
|
||||
}
|
||||
|
||||
set progressUpdateInterval(value: number) {
|
||||
this._progressUpdateInterval = value;
|
||||
this.stopProgressTimer();
|
||||
if (!this._paused) {
|
||||
this.startProgressTimer();
|
||||
}
|
||||
}
|
||||
|
||||
set rate(value: number) {
|
||||
this.videoElement.defaultPlaybackRate = value; // playbackRate doesn't work on Chrome
|
||||
this.videoElement.playbackRate = value;
|
||||
}
|
||||
|
||||
set repeat(value: boolean) {
|
||||
this.videoElement.loop = value;
|
||||
}
|
||||
|
||||
set resizeMode(value: number) {
|
||||
switch (value) {
|
||||
case resizeModes.ScaleNone: {
|
||||
this.videoElement.style.objectFit = "none";
|
||||
break;
|
||||
}
|
||||
case resizeModes.ScaleToFill: {
|
||||
this.videoElement.style.objectFit = "fill";
|
||||
break;
|
||||
}
|
||||
case resizeModes.ScaleAspectFit: {
|
||||
this.videoElement.style.objectFit = "contain";
|
||||
break;
|
||||
}
|
||||
case resizeModes.ScaleAspectFill: {
|
||||
this.videoElement.style.objectFit = "cover";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set seek(value: number) {
|
||||
this.videoElement.currentTime = value;
|
||||
}
|
||||
|
||||
set source(value: VideoSource) {
|
||||
let uri = value.uri;
|
||||
|
||||
if (uri.startsWith("blob:")) {
|
||||
let blob = this.bridge.blobManager.resolveURL(uri);
|
||||
if (blob.type === "text/xml") {
|
||||
blob = new Blob([blob], { type: "video/mp4" });
|
||||
}
|
||||
uri = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
if (!shaka.Player.isBrowserSupported()) { // primarily iOS WebKit
|
||||
this.videoElement.setAttribute("src", uri);
|
||||
if (!this._paused) {
|
||||
this.requestPlay();
|
||||
}
|
||||
} else {
|
||||
this.player.load(uri)
|
||||
.then(() => {
|
||||
if (!this._paused) {
|
||||
this.requestPlay();
|
||||
}
|
||||
})
|
||||
.catch(this.onError);
|
||||
}
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
if (value === 0) {
|
||||
this.muted = true;
|
||||
} else {
|
||||
this.videoElement.volume = value;
|
||||
this.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
onEnd = () => {
|
||||
this.onProgress();
|
||||
this.sendEvent("topVideoEnd", null);
|
||||
this.stopProgressTimer();
|
||||
}
|
||||
|
||||
onError = error => {
|
||||
console.warn("topVideoError", error);
|
||||
}
|
||||
|
||||
onLoad = () => {
|
||||
// height & width are safe with audio, will be 0
|
||||
const height = this.videoElement.videoHeight;
|
||||
const width = this.videoElement.videoWidth;
|
||||
const payload = {
|
||||
currentPosition: this.videoElement.currentTime,
|
||||
duration: this.videoElement.duration,
|
||||
naturalSize: {
|
||||
width,
|
||||
height,
|
||||
orientation: width >= height ? "landscape" : "portrait"
|
||||
}
|
||||
};
|
||||
this.sendEvent("topVideoLoad", payload);
|
||||
}
|
||||
|
||||
onReadyForDisplay = () => {
|
||||
this.sendEvent("onReadyForDisplay");
|
||||
}
|
||||
|
||||
onLoadStart = () => {
|
||||
const src = this.videoElement.currentSrc;
|
||||
const payload = {
|
||||
isNetwork: !src.match(/^https?:\/\/localhost/), // require is served from localhost
|
||||
uri: this.videoElement.currentSrc
|
||||
};
|
||||
this.sendEvent("topVideoLoadStart", payload);
|
||||
}
|
||||
|
||||
onPause = () => {
|
||||
this.stopProgressTimer();
|
||||
}
|
||||
|
||||
onPlay = () => {
|
||||
this.startProgressTimer();
|
||||
}
|
||||
|
||||
onProgress = () => {
|
||||
const payload = {
|
||||
currentTime: this.videoElement.currentTime,
|
||||
seekableDuration: this.videoElement.duration
|
||||
};
|
||||
this.sendEvent("topVideoProgress", payload);
|
||||
}
|
||||
|
||||
onRejectedAutoplay = () => {
|
||||
this.sendEvent("topVideoRejectedAutoplay", null);
|
||||
}
|
||||
|
||||
requestPlay() {
|
||||
const playPromise = this.videoElement.play();
|
||||
if (playPromise) {
|
||||
playPromise
|
||||
.then(() => {})
|
||||
.catch(e => {
|
||||
/* This is likely one of:
|
||||
* name: NotAllowedError - autoplay is not supported
|
||||
* name: NotSupportedError - format is not supported
|
||||
*/
|
||||
this.onError({ code: e.name, message: e.message });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(eventName, payload) {
|
||||
const event = new RCTVideoEvent(eventName, this.reactTag, 0, payload);
|
||||
this.eventDispatcher.sendEvent(event);
|
||||
}
|
||||
|
||||
startProgressTimer() {
|
||||
if (!this.progressTimer && this._progressUpdateInterval) {
|
||||
this.onProgress();
|
||||
this.progressTimer = setInterval(this.onProgress, this._progressUpdateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
stopProgressTimer() {
|
||||
if (this.progressTimer) {
|
||||
clearInterval(this.progressTimer);
|
||||
this.progressTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rct-video", RCTVideo);
|
||||
|
||||
export default RCTVideo;
|
@ -1,56 +0,0 @@
|
||||
// import { RCTEvent } from "react-native-dom";
|
||||
|
||||
interface RCTEvent {
|
||||
viewTag: number;
|
||||
eventName: string;
|
||||
coalescingKey: number;
|
||||
|
||||
canCoalesce(): boolean;
|
||||
coalesceWithEvent(event: RCTEvent): RCTEvent;
|
||||
|
||||
moduleDotMethod(): string;
|
||||
arguments(): Array<any>;
|
||||
}
|
||||
|
||||
export default class RCTVideoEvent implements RCTEvent {
|
||||
viewTag: number;
|
||||
eventName: string;
|
||||
coalescingKey: number;
|
||||
|
||||
constructor(
|
||||
eventName: string,
|
||||
reactTag: number,
|
||||
coalescingKey: number,
|
||||
data: ?Object
|
||||
) {
|
||||
this.viewTag = reactTag;
|
||||
this.eventName = eventName;
|
||||
this.coalescingKey = coalescingKey;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
canCoalesce(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
coalesceWithEvent(event: RCTEvent): RCTEvent {
|
||||
return;
|
||||
}
|
||||
|
||||
moduleDotMethod(): string {
|
||||
return "RCTEventEmitter.receiveEvent";
|
||||
}
|
||||
|
||||
arguments(): Array<any> {
|
||||
const args = [
|
||||
this.viewTag,
|
||||
this.eventName,
|
||||
this.data
|
||||
];
|
||||
return args;
|
||||
}
|
||||
|
||||
coalescingKey(): number {
|
||||
return this.coalescingKey;
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { RCTViewManager } from "react-native-dom";
|
||||
|
||||
import RCTVideo from "./RCTVideo";
|
||||
import resizeModes from "./resizeModes";
|
||||
|
||||
import type { VideoSource } from "./types";
|
||||
|
||||
class RCTVideoManager extends RCTViewManager {
|
||||
static moduleName = "RCTVideoManager";
|
||||
|
||||
view() {
|
||||
return new RCTVideo(this.bridge);
|
||||
}
|
||||
|
||||
describeProps() {
|
||||
return super
|
||||
.describeProps()
|
||||
.addBooleanProp("controls", this.setControls)
|
||||
.addStringProp("id", this.setId)
|
||||
.addBooleanProp("muted", this.setMuted)
|
||||
.addBooleanProp("paused", this.setPaused)
|
||||
.addNumberProp("progressUpdateInterval", this.setProgressUpdateInterval)
|
||||
.addBooleanProp("rate", this.setRate)
|
||||
.addBooleanProp("repeat", this.setRepeat)
|
||||
.addNumberProp("resizeMode", this.setResizeMode)
|
||||
.addNumberProp("seek", this.setSeek)
|
||||
.addObjectProp("src", this.setSource)
|
||||
.addNumberProp("volume", this.setVolume)
|
||||
.addDirectEvent("onVideoEnd")
|
||||
.addDirectEvent("onVideoError")
|
||||
.addDirectEvent("onVideoLoad")
|
||||
.addDirectEvent("onVideoLoadStart")
|
||||
.addDirectEvent("onVideoProgress");
|
||||
}
|
||||
|
||||
dismissFullscreenPlayer() {
|
||||
// not currently working
|
||||
}
|
||||
|
||||
presentFullscreenPlayer() {
|
||||
// not currently working
|
||||
}
|
||||
|
||||
setControls(view: RCTVideo, value: boolean) {
|
||||
view.controls = value;
|
||||
}
|
||||
|
||||
setId(view: RCTVideo, value: string) {
|
||||
view.id = value;
|
||||
}
|
||||
|
||||
setMuted(view: RCTVideo, value: boolean) {
|
||||
view.muted = value;
|
||||
}
|
||||
|
||||
setPaused(view: RCTVideo, value: boolean) {
|
||||
view.paused = value;
|
||||
}
|
||||
|
||||
setRate(view: RCTVideo, value: number) {
|
||||
view.rate = value;
|
||||
}
|
||||
|
||||
setRepeat(view: RCTVideo, value: boolean) {
|
||||
view.repeat = value;
|
||||
}
|
||||
|
||||
setResizeMode(view: RCTVideo, value: number) {
|
||||
view.resizeMode = value;
|
||||
}
|
||||
|
||||
setSeek(view: RCTVideo, value: number) {
|
||||
view.seek = value;
|
||||
}
|
||||
|
||||
setSource(view: RCTVideo, value: VideoSource) {
|
||||
view.source = value;
|
||||
}
|
||||
|
||||
constantsToExport() {
|
||||
return { ...resizeModes };
|
||||
}
|
||||
}
|
||||
|
||||
export default RCTVideoManager;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
|
||||
module.exports = require("./RCTVideoManager");
|
@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export default {
|
||||
ScaleNone: 0,
|
||||
ScaleToFill: 1,
|
||||
ScaleAspectFit: 2,
|
||||
ScaleAspectFill: 3,
|
||||
};
|
10
dom/types.js
@ -1,10 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export type VideoSource = {
|
||||
uri: string,
|
||||
type: string,
|
||||
mainVer: number,
|
||||
patchVer: number,
|
||||
isNetwork: boolean,
|
||||
isAsset: boolean,
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
|
||||
[android]
|
||||
target = Google Inc.:Google APIs:23
|
||||
|
||||
[maven_repositories]
|
||||
central = https://repo1.maven.org/maven2
|
@ -1,58 +0,0 @@
|
||||
[ignore]
|
||||
; We fork some components by platform
|
||||
.*/*[.]android.js
|
||||
|
||||
# We fork some components by platform.
|
||||
.*/*[.]android.js
|
||||
|
||||
# Ignore templates with `@flow` in header
|
||||
.*/local-cli/generator.*
|
||||
|
||||
# Ignore malformed json
|
||||
.*/node_modules/y18n/test/.*\.json
|
||||
|
||||
# Ignore the website subdir
|
||||
<PROJECT_ROOT>/website/.*
|
||||
|
||||
# Ignore BUCK generated dirs
|
||||
|
||||
<PROJECT_ROOT>/\.buckd/
|
||||
|
||||
; Ignore unexpected extra "@providesModule"
|
||||
.*/node_modules/.*/node_modules/fbjs/.*
|
||||
|
||||
; Ignore duplicate module providers
|
||||
; For RN Apps installed via npm, "Libraries" folder is inside
|
||||
; "node_modules/react-native" but in the source repo it is in the root
|
||||
.*/Libraries/react-native/React.js
|
||||
.*/Libraries/react-native/ReactNative.js
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
node_modules/react-native/Libraries/react-native/react-native-interface.js
|
||||
node_modules/react-native/flow
|
||||
flow/
|
||||
|
||||
[options]
|
||||
module.system=haste
|
||||
|
||||
experimental.strict_type_args=true
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-6]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-6]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
|
||||
^0.36.0
|
1
examples/basic/.gitattributes
vendored
@ -1 +0,0 @@
|
||||
*.pbxproj -text
|
53
examples/basic/.gitignore
vendored
@ -1,53 +0,0 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
*.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
@ -1 +0,0 @@
|
||||
{}
|
14
examples/basic/__tests__/App-test.js
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import VideoPlayer from '../src/VideoPlayer';
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderer.create(<VideoPlayer />);
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
import re
|
||||
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
@ -10,57 +8,48 @@ import re
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
lib_deps = []
|
||||
for jarfile in glob(['libs/*.jar']):
|
||||
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
|
||||
lib_deps.append(':' + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
for aarfile in glob(['libs/*.aar']):
|
||||
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
|
||||
lib_deps.append(':' + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = 'all-libs',
|
||||
exported_deps = lib_deps
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = 'app-code',
|
||||
srcs = glob([
|
||||
'src/main/java/**/*.java',
|
||||
]),
|
||||
deps = [
|
||||
':all-libs',
|
||||
':build_config',
|
||||
':res',
|
||||
],
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = 'build_config',
|
||||
package = 'com.videoplayer',
|
||||
name = "build_config",
|
||||
package = "com.videoplayer",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = 'res',
|
||||
res = 'src/main/res',
|
||||
package = 'com.videoplayer',
|
||||
name = "res",
|
||||
package = "com.videoplayer",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = 'app',
|
||||
package_type = 'debug',
|
||||
manifest = 'src/main/AndroidManifest.xml',
|
||||
keystore = '//android/keystores:debug',
|
||||
deps = [
|
||||
':app-code',
|
||||
],
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
|
@ -15,9 +15,14 @@ import com.android.build.OutputFile
|
||||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||
* bundleCommand: "ram-bundle",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
* bundleInDebug: false,
|
||||
*
|
||||
@ -33,6 +38,13 @@ import com.android.build.OutputFile
|
||||
* // bundleInPaidRelease: true,
|
||||
* // bundleInBeta: true,
|
||||
*
|
||||
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||
* // for example: to disable dev mode in the staging build type (if configured)
|
||||
* devDisabledInStaging: true,
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||
* // 'devDisabledIn${buildType}'
|
||||
*
|
||||
* // the root of your project, i.e. where "package.json" lives
|
||||
* root: "../../",
|
||||
*
|
||||
@ -58,7 +70,7 @@ import com.android.build.OutputFile
|
||||
* inputExcludes: ["android/**", "ios/**"],
|
||||
*
|
||||
* // override which node gets called and with what additional arguments
|
||||
* nodeExecutableAndArgs: ["node"]
|
||||
* nodeExecutableAndArgs: ["node"],
|
||||
*
|
||||
* // supply additional arguments to the packager
|
||||
* extraPackagerArgs: []
|
||||
@ -66,9 +78,9 @@ import com.android.build.OutputFile
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.android.js",
|
||||
enableHermes: false,
|
||||
enableHermes: false, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
|
||||
/**
|
||||
@ -86,58 +98,117 @@ def enableSeparateBuildPerCPUArchitecture = false
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore.
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.videoplayer"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
multiDexEnabled true
|
||||
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86"
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
matchingFallbacks = ['release', 'debug']
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2]
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':react-native-video')
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation 'org.webkit:android-jsc:+'
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
@ -146,3 +217,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
19
examples/basic/android/app/build_defs.bzl
Normal file
@ -0,0 +1,19 @@
|
||||
"""Helper definitions to glob .aar and .jar targets"""
|
||||
|
||||
def create_aar_targets(aarfiles):
|
||||
for aarfile in aarfiles:
|
||||
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
|
||||
lib_deps.append(":" + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
|
||||
def create_jar_targets(jarfiles):
|
||||
for jarfile in jarfiles:
|
||||
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
|
||||
lib_deps.append(":" + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
BIN
examples/basic/android/app/debug.keystore
Normal file
@ -63,4 +63,4 @@
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
-dontwarn okio.**
|
8
examples/basic/android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.videoplayer;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,35 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.videoplayer"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
package="com.videoplayer">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
@ -4,12 +4,12 @@ import com.facebook.react.ReactActivity;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "VideoPlayer";
|
||||
}
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "VideoPlayer";
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
package com.videoplayer;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
@ -22,10 +21,17 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.asList(
|
||||
new MainReactPackage(),
|
||||
new ReactVideoPackage()
|
||||
);
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for
|
||||
// example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "src/index";
|
||||
}
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 15 KiB |
@ -3,6 +3,7 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:textColor">#000000</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -2,20 +2,17 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "28.0.3"
|
||||
minSdkVersion = 16
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 28
|
||||
supportLibVersion = "28.0.0"
|
||||
|
||||
}
|
||||
buildToolsVersion = "30.0.2"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 29
|
||||
}
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
|
||||
classpath("com.android.tools.build:gradle:4.2.2")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
@ -24,18 +21,17 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
}
|
||||
maven {
|
||||
// Android JSC is installed from npm
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
}
|
||||
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
@ -17,5 +17,12 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.54.0
|
||||
|
@ -1,6 +1,5 @@
|
||||
#Thu Mar 09 08:03:03 PST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
|
147
examples/basic/android/gradlew
vendored
@ -1,4 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
@ -6,47 +22,6 @@
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
@ -61,9 +36,49 @@ while [ -h "$PRG" ] ; do
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
@ -90,7 +105,7 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
@ -110,10 +125,11 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
@ -138,27 +154,30 @@ if $cygwin ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
193
examples/basic/android/gradlew.bat
vendored
@ -1,90 +1,103 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
@ -1,8 +0,0 @@
|
||||
keystore(
|
||||
name = 'debug',
|
||||
store = 'debug.keystore',
|
||||
properties = 'debug.keystore.properties',
|
||||
visibility = [
|
||||
'PUBLIC',
|
||||
],
|
||||
)
|
@ -1,4 +0,0 @@
|
||||
key.store=debug.keystore
|
||||
key.alias=androiddebugkey
|
||||
key.store.password=android
|
||||
key.alias.password=android
|
@ -1,6 +1,5 @@
|
||||
rootProject.name = 'VideoPlayer'
|
||||
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||
include ':app'
|
||||
|
4
examples/basic/app.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "VideoPlayer",
|
||||
"displayName": "VideoPlayer"
|
||||
}
|
@ -1,3 +1,16 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
};
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
extensions: ['.js', '.tsx', '.ts'],
|
||||
root: ['./src'],
|
||||
|
||||
alias: {
|
||||
src: './src',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
@ -1,230 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React, {
|
||||
Component
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
AppRegistry,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import Video from 'react-native-video';
|
||||
|
||||
class VideoPlayer extends Component {
|
||||
|
||||
state = {
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
resizeMode: 'contain',
|
||||
duration: 0.0,
|
||||
currentTime: 0.0,
|
||||
paused: true,
|
||||
};
|
||||
|
||||
video: Video;
|
||||
|
||||
onLoad = (data) => {
|
||||
this.setState({ duration: data.duration });
|
||||
};
|
||||
|
||||
onProgress = (data) => {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
};
|
||||
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true })
|
||||
this.video.seek(0)
|
||||
};
|
||||
|
||||
onAudioBecomingNoisy = () => {
|
||||
this.setState({ paused: true })
|
||||
};
|
||||
|
||||
onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
|
||||
this.setState({ paused: !event.hasAudioFocus })
|
||||
};
|
||||
|
||||
getCurrentTimePercentage() {
|
||||
if (this.state.currentTime > 0) {
|
||||
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
renderRateControl(rate) {
|
||||
const isSelected = (this.state.rate === rate);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => { this.setState({ rate }) }}>
|
||||
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
|
||||
{rate}x
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderResizeModeControl(resizeMode) {
|
||||
const isSelected = (this.state.resizeMode === resizeMode);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => { this.setState({ resizeMode }) }}>
|
||||
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
|
||||
{resizeMode}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
renderVolumeControl(volume) {
|
||||
const isSelected = (this.state.volume === volume);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => { this.setState({ volume }) }}>
|
||||
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
|
||||
{volume * 100}%
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const flexCompleted = this.getCurrentTimePercentage() * 100;
|
||||
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity
|
||||
style={styles.fullScreen}
|
||||
onPress={() => this.setState({ paused: !this.state.paused })}
|
||||
>
|
||||
<Video
|
||||
ref={(ref: Video) => { this.video = ref }}
|
||||
/* For ExoPlayer */
|
||||
/* source={{ uri: 'http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0', type: 'mpd' }} */
|
||||
source={require('./broadchurch.mp4')}
|
||||
style={styles.fullScreen}
|
||||
rate={this.state.rate}
|
||||
paused={this.state.paused}
|
||||
volume={this.state.volume}
|
||||
muted={this.state.muted}
|
||||
resizeMode={this.state.resizeMode}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
onAudioBecomingNoisy={this.onAudioBecomingNoisy}
|
||||
onAudioFocusChanged={this.onAudioFocusChanged}
|
||||
repeat={false}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.controls}>
|
||||
<View style={styles.generalControls}>
|
||||
<View style={styles.rateControl}>
|
||||
{this.renderRateControl(0.25)}
|
||||
{this.renderRateControl(0.5)}
|
||||
{this.renderRateControl(1.0)}
|
||||
{this.renderRateControl(1.5)}
|
||||
{this.renderRateControl(2.0)}
|
||||
</View>
|
||||
|
||||
<View style={styles.volumeControl}>
|
||||
{this.renderVolumeControl(0.5)}
|
||||
{this.renderVolumeControl(1)}
|
||||
{this.renderVolumeControl(1.5)}
|
||||
</View>
|
||||
|
||||
<View style={styles.resizeModeControl}>
|
||||
{this.renderResizeModeControl('cover')}
|
||||
{this.renderResizeModeControl('contain')}
|
||||
{this.renderResizeModeControl('stretch')}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.trackingControls}>
|
||||
<View style={styles.progress}>
|
||||
<View style={[styles.innerProgressCompleted, { flex: flexCompleted }]} />
|
||||
<View style={[styles.innerProgressRemaining, { flex: flexRemaining }]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
fullScreen: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
controls: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 5,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
progress: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerProgressCompleted: {
|
||||
height: 20,
|
||||
backgroundColor: '#cccccc',
|
||||
},
|
||||
innerProgressRemaining: {
|
||||
height: 20,
|
||||
backgroundColor: '#2C2C2C',
|
||||
},
|
||||
generalControls: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
rateControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
volumeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resizeModeControl: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
controlOption: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 11,
|
||||
color: 'white',
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
lineHeight: 12,
|
||||
},
|
||||
});
|
||||
|
||||
AppRegistry.registerComponent('VideoPlayer', () => VideoPlayer);
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIWindow *window;
|
||||
|
||||
@end
|