Merge branch 'master' of https://github.com/react-native-video/react-native-video into fix/report_time_position_when_updated
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
5aeb49e094
3
.github/ISSUE_TEMPLATE.md
vendored
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
|
||||
|
||||
|
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -18,12 +18,11 @@ assignees: ''
|
||||
|
||||
## 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
|
||||
|
||||
|
211
API.md
211
API.md
@ -5,7 +5,6 @@
|
||||
* [tvOS](#tvos-installation)
|
||||
* [Android](#android-installation)
|
||||
* [Windows](#windows-installation)
|
||||
* [react-native-dom](#react-native-dom-installation)
|
||||
* [Examples](#examples)
|
||||
* [iOS](#ios-example)
|
||||
* [Android](#android-example)
|
||||
@ -111,14 +110,7 @@ Or if you have trouble, make the following additions to the given files manually
|
||||
|
||||
#### **android/settings.gradle**
|
||||
|
||||
The newer ExoPlayer library will work for most people.
|
||||
|
||||
```gradle
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
```
|
||||
|
||||
If you need to use the old Android MediaPlayer based player, use the following instead:
|
||||
Add player source in build configuration
|
||||
|
||||
```gradle
|
||||
include ':react-native-video'
|
||||
@ -220,32 +212,6 @@ Follow the manual linking instuctions for React Native Windows 0.62 above, but s
|
||||
|
||||
</details>
|
||||
|
||||
### react-native-dom installation
|
||||
<details>
|
||||
<summary>react-native-dom details</summary>
|
||||
|
||||
Make the following additions to the given files manually:
|
||||
|
||||
#### **dom/bootstrap.js**
|
||||
|
||||
Import RCTVideoManager and add it to the list of nativeModules:
|
||||
|
||||
```javascript
|
||||
import { RNDomInstance } from "react-native-dom";
|
||||
import { name as appName } from "../app.json";
|
||||
import RCTVideoManager from 'react-native-video/dom/RCTVideoManager'; // Add this
|
||||
|
||||
// Path to RN Bundle Entrypoint ================================================
|
||||
const rnBundlePath = "./entry.bundle?platform=dom&dev=true";
|
||||
|
||||
// React Native DOM Runtime Options =============================================
|
||||
const ReactNativeDomOptions = {
|
||||
enableHotReload: false,
|
||||
nativeModules: [RCTVideoManager] // Add this
|
||||
};
|
||||
```
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
Run `yarn xbasic install` before running any of the examples.
|
||||
@ -302,29 +268,28 @@ var styles = StyleSheet.create({
|
||||
|[allowsExternalPlayback](#allowsexternalplayback) |iOS |
|
||||
|[audioOnly](#audioonly)|All |
|
||||
|[automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) | iOS|\
|
||||
|[backBufferDurationMs](#backBufferDurationMs)| Android Exoplayer|
|
||||
|[bufferConfig](#bufferconfig)|Android ExoPlayer|
|
||||
|[contentStartTime](#contentStartTime)| Android Exoplayer|
|
||||
|[controls](#controls)|Android ExoPlayer, iOS, react-native-dom|
|
||||
|[currentPlaybackTime](#currentPlaybackTime)|Android Exoplayer|
|
||||
|[disableFocus](#disableFocus)|Android Exoplayer, iOS|
|
||||
|[disableDisconnectError](#disableDisconnectError)|Android Exoplayer|
|
||||
|[backBufferDurationMs](#backBufferDurationMs)| Android |
|
||||
|[bufferConfig](#bufferconfig)|Android|
|
||||
|[contentStartTime](#contentStartTime)| Android |
|
||||
|[controls](#controls)|Android, iOS|
|
||||
|[currentPlaybackTime](#currentPlaybackTime)|Android|
|
||||
|[disableFocus](#disableFocus)|Android, iOS|
|
||||
|[disableDisconnectError](#disableDisconnectError)|Android|
|
||||
|[filter](#filter)|iOS|
|
||||
|[filterEnabled](#filterEnabled)|iOS|
|
||||
|[fullscreen](#fullscreen)|iOS|
|
||||
|[fullscreenAutorotate](#fullscreenautorotate)|iOS|
|
||||
|[fullscreenOrientation](#fullscreenorientation)|iOS|
|
||||
|[headers](#headers)|Android ExoPlayer|
|
||||
|[hideShutterView](#hideshutterview)|Android ExoPlayer|
|
||||
|[id](#id)|react-native-dom|
|
||||
|[headers](#headers)|Android|
|
||||
|[hideShutterView](#hideshutterview)|Android|
|
||||
|[ignoreSilentSwitch](#ignoresilentswitch)|iOS|
|
||||
|[maxBitRate](#maxbitrate)|Android ExoPlayer, iOS|
|
||||
|[minLoadRetryCount](#minLoadRetryCount)|Android ExoPlayer|
|
||||
|[maxBitRate](#maxbitrate)|Android, iOS|
|
||||
|[minLoadRetryCount](#minLoadRetryCount)|Android|
|
||||
|[mixWithOthers](#mixWithOthers)|iOS|
|
||||
|[muted](#muted)|All|
|
||||
|[paused](#paused)|All|
|
||||
|[pictureInPicture](#pictureinpicture)|iOS|
|
||||
|[playInBackground](#playinbackground)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[playInBackground](#playinbackground)|Android, iOS|
|
||||
|[playWhenInactive](#playwheninactive)|iOS|
|
||||
|[poster](#poster)|All|
|
||||
|[posterResizeMode](#posterresizemode)|All|
|
||||
@ -333,17 +298,16 @@ var styles = StyleSheet.create({
|
||||
|[progressUpdateInterval](#progressupdateinterval)|All|
|
||||
|[rate](#rate)|All|
|
||||
|[repeat](#repeat)|All|
|
||||
|[reportBandwidth](#reportbandwidth)|Android ExoPlayer|
|
||||
|[resizeMode](#resizemode)|Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP|
|
||||
|[selectedAudioTrack](#selectedaudiotrack)|Android ExoPlayer, iOS|
|
||||
|[selectedTextTrack](#selectedtexttrack)|Android ExoPlayer, iOS|
|
||||
|[selectedVideoTrack](#selectedvideotrack)|Android ExoPlayer|
|
||||
|[reportBandwidth](#reportbandwidth)|Android|
|
||||
|[resizeMode](#resizemode)|Android, iOS, Windows UWP|
|
||||
|[selectedAudioTrack](#selectedaudiotrack)|Android, iOS|
|
||||
|[selectedTextTrack](#selectedtexttrack)|Android, iOS|
|
||||
|[selectedVideoTrack](#selectedvideotrack)|Android|
|
||||
|[source](#source)|All|
|
||||
|[stereoPan](#stereopan)|Android MediaPlayer|
|
||||
|[textTracks](#texttracks)|Android ExoPlayer, iOS|
|
||||
|[trackId](#trackId)|Android ExoPlayer|
|
||||
|[useTextureView](#usetextureview)|Android ExoPlayer|
|
||||
|[useSecureView](#useSecureView)|Android Exoplayer|
|
||||
|[textTracks](#texttracks)|Android, iOS|
|
||||
|[trackId](#trackId)|Android|
|
||||
|[useTextureView](#usetextureview)|Android|
|
||||
|[useSecureView](#useSecureView)|Android|
|
||||
|[volume](#volume)|All|
|
||||
|[localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme)|All|
|
||||
|
||||
@ -351,31 +315,31 @@ var styles = StyleSheet.create({
|
||||
### Event props
|
||||
| Name |Plateforms Support |
|
||||
|--|--|
|
||||
|[onAudioBecomingNoisy](#onaudiobecomingnoisy)|Android ExoPlayer, iOS|
|
||||
|[onBandwidthUpdate](#onbandwidthupdate)|Android ExoPlayer|
|
||||
|[onBuffer](#onbuffer)|Android ExoPlayer, iOS|
|
||||
|[onAudioBecomingNoisy](#onaudiobecomingnoisy)|Android, iOS|
|
||||
|[onBandwidthUpdate](#onbandwidthupdate)|Android|
|
||||
|[onBuffer](#onbuffer)|Android, iOS|
|
||||
|[onEnd](#onend)|All|
|
||||
|[onExternalPlaybackChange](#onexternalplaybackchange)|iOS|
|
||||
|[onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent)|Android, iOS|
|
||||
|[onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent)|Android, iOS|
|
||||
|[onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss)|Android, iOS|
|
||||
|[onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)|Android, iOS|
|
||||
|[onLoad](#onload)|All|
|
||||
|[onLoadStart](#onloadstart)|All|
|
||||
|[onReadyForDisplay](#onreadyfordisplay)|Android ExoPlayer, Android MediaPlayer, iOS, Web|
|
||||
|[onReadyForDisplay](#onreadyfordisplay)|Android, iOS, Web|
|
||||
|[onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged)|iOS|
|
||||
|[onPlaybackRateChange](#onplaybackratechange)|All|
|
||||
|[onProgress](#onprogress)|All|
|
||||
|[onSeek](#onseek)|Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP|
|
||||
|[onSeek](#onseek)|Android, iOS, Windows UWP|
|
||||
|[onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop)|iOS|
|
||||
|[onTimedMetadata](#ontimedmetadata)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[onTimedMetadata](#ontimedmetadata)|Android, iOS|
|
||||
|
||||
|
||||
### Methods
|
||||
| Name |Plateforms Support |
|
||||
|--|--|
|
||||
|[dismissFullscreenPlayer](#dismissfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[presentFullscreenPlayer](#presentfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS|
|
||||
|[dismissFullscreenPlayer](#dismissfullscreenplayer)|Android, iOS|
|
||||
|[presentFullscreenPlayer](#presentfullscreenplayer)|Android, iOS|
|
||||
|[save](#save)|iOS|
|
||||
|[restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)|iOS|
|
||||
|[seek](#seek)|All|
|
||||
@ -409,7 +373,7 @@ Platforms: iOS
|
||||
#### backBufferDurationMs
|
||||
The number of milliseconds of buffer to keep before the current position. This allows rewinding without rebuffering within that duration.
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### bufferConfig
|
||||
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
||||
@ -436,12 +400,12 @@ bufferConfig={{
|
||||
}}
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### currentPlaybackTime
|
||||
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.
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### controls
|
||||
Determines whether to show player controls.
|
||||
@ -453,30 +417,28 @@ Note on iOS, controls are always shown when in fullscreen mode.
|
||||
### contentStartTime
|
||||
The start time in ms for SSAI content. This determines at what time to load the video info like resolutions. Use this only when you have SSAI stream where ads resolution is not the same as content resolution.
|
||||
|
||||
For Android MediaPlayer, you will need to build your own controls or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-video-player](https://github.com/cornedor/react-native-video-player).
|
||||
Note on Android, native controls are available by default. If needed, you can also add your controls or use a package like [react-native-video-controls].
|
||||
|
||||
Note on Android ExoPlayer, native controls are available by default. If needed, you can also add your controls or use a package like [react-native-video-controls].
|
||||
|
||||
Platforms: Android ExoPlayer, iOS, react-native-dom
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### disableFocus
|
||||
Determines whether video audio should override background music/audio in Android devices.
|
||||
* ** false (default)** - Override background audio/music
|
||||
* **true** - Let background audio/music from other apps play
|
||||
|
||||
Platforms: Android Exoplayer
|
||||
Platforms: Android
|
||||
|
||||
#### disableDisconnectError
|
||||
Determines if the player needs to throw an error when connection is lost or not
|
||||
* **false (default)** - Player will throw an error when connection is lost
|
||||
* **true** - Player will keep trying to buffer when network connect is lost
|
||||
|
||||
Platforms: Android Exoplayer
|
||||
Platforms: Android
|
||||
|
||||
### DRM
|
||||
To setup DRM please follow [this guide](./DRM.md)
|
||||
|
||||
Platforms: Android Exoplayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### filter
|
||||
Add video filter
|
||||
@ -548,7 +510,7 @@ source={{
|
||||
}}
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### hideShutterView
|
||||
Controls whether the ExoPlayer shutter view (black screen while loading) is enabled.
|
||||
@ -556,17 +518,7 @@ Controls whether the ExoPlayer shutter view (black screen while loading) is enab
|
||||
* **false (default)** - Show shutter view
|
||||
* **true** - Hide shutter view
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
|
||||
#### id
|
||||
Set the DOM id element so you can use document.getElementById on web platforms. Accepts string values.
|
||||
|
||||
Example:
|
||||
```
|
||||
id="video"
|
||||
```
|
||||
|
||||
Platforms: react-native-dom
|
||||
Platforms: Android
|
||||
|
||||
#### ignoreSilentSwitch
|
||||
Controls the iOS silent switch behavior
|
||||
@ -586,7 +538,7 @@ Example:
|
||||
maxBitRate={2000000} // 2 megabits
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### minLoadRetryCount
|
||||
Sets the minimum number of times to retry loading data before failing and reporting an error to the application. Useful to recover from transient internet failures.
|
||||
@ -598,7 +550,7 @@ Example:
|
||||
minLoadRetryCount={5} // retry 5 times
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### mixWithOthers
|
||||
Controls how Audio mix with other apps.
|
||||
@ -638,7 +590,7 @@ To use this feature on iOS, you must:
|
||||
* [Enable Background Audio](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionBasics/AudioSessionBasics.html#//apple_ref/doc/uid/TP40007875-CH3-SW3) in your Xcode project
|
||||
* Set the ignoreSilentSwitch prop to "ignore"
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### playWhenInactive
|
||||
Determine whether the media should continue playing when notifications or the Control Center are in front of the video.
|
||||
@ -693,8 +645,6 @@ Speed at which the media should play.
|
||||
|
||||
Platforms: all
|
||||
|
||||
Note: For Android MediaPlayer, rate is only supported on Android 6.0 and higher devices.
|
||||
|
||||
#### repeat
|
||||
Determine whether to repeat the video when the end is reached
|
||||
* **false (default)** - Don't repeat the video
|
||||
@ -708,7 +658,7 @@ Determine whether to generate onBandwidthUpdate events. This is needed due to th
|
||||
* **false (default)** - Don't generate onBandwidthUpdate events
|
||||
* **true** - Generate onBandwidthUpdate events
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### resizeMode
|
||||
Determines how to resize the video when the frame doesn't match the raw video dimensions.
|
||||
@ -717,7 +667,7 @@ Determines how to resize the video when the frame doesn't match the raw video di
|
||||
* **"cover"** - Scale the video uniformly (maintain the video's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding).
|
||||
* **"stretch"** - Scale width and height independently, This may change the aspect ratio of the src.
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
|
||||
Platforms: Android, iOS, Windows UWP
|
||||
|
||||
#### selectedAudioTrack
|
||||
Configure which audio track, if any, is played.
|
||||
@ -747,7 +697,7 @@ Type | Value | Description
|
||||
|
||||
If a track matching the specified Type (and Value if appropriate) is unavailable, the first audio track will be played. If multiple tracks match the criteria, the first match will be used.
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### selectedTextTrack
|
||||
Configure which text track (caption or subtitle), if any, is shown.
|
||||
@ -779,7 +729,7 @@ Both iOS & Android (only 4.4 and higher) offer Settings to enable Captions for h
|
||||
|
||||
If a track matching the specified Type (and Value if appropriate) is unavailable, no text track will be displayed. If multiple tracks match the criteria, the first match will be used.
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### selectedVideoTrack
|
||||
Configure which video track should be played. By default, the player uses Adaptive Bitrate Streaming to automatically select the stream it thinks will perform best based on available bandwidth.
|
||||
@ -808,7 +758,7 @@ Type | Value | Description
|
||||
|
||||
If a track matching the specified Type (and Value if appropriate) is unavailable, ABR will be used.
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### source
|
||||
Sets the media source. You can pass an asset loaded via require or an object with a uri.
|
||||
@ -853,7 +803,7 @@ source={{ uri: 'file:///sdcard/Movies/sintel.mp4' }}
|
||||
|
||||
Note: Your app will need to request permission to read external storage if you're accessing a file outside your app.
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, possibly others
|
||||
Platforms: Android, possibly others
|
||||
|
||||
###### iPod Library (ipod-library://)
|
||||
|
||||
@ -885,14 +835,6 @@ The following other types are supported on some platforms, but aren't fully docu
|
||||
`content://, ms-appx://, ms-appdata://, assets-library://`
|
||||
|
||||
|
||||
#### stereoPan
|
||||
Adjust the balance of the left and right audio channels. Any value between –1.0 and 1.0 is accepted.
|
||||
* **-1.0** - Full left
|
||||
* **0.0 (default)** - Center
|
||||
* **1.0** - Full right
|
||||
|
||||
Platforms: Android MediaPlayer
|
||||
|
||||
#### textTracks
|
||||
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||
|
||||
@ -900,7 +842,7 @@ Property | Description
|
||||
--- | ---
|
||||
title | Descriptive name for the track
|
||||
language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language
|
||||
type | Mime type of the track<br> * TextTrackType.SRT - SubRip (.srt)<br> * TextTrackType.TTML - TTML (.ttml)<br> * TextTrackType.VTT - WebVTT (.vtt)<br>iOS only supports VTT, Android ExoPlayer supports all 3
|
||||
type | Mime type of the track<br> * TextTrackType.SRT - SubRip (.srt)<br> * TextTrackType.TTML - TTML (.ttml)<br> * TextTrackType.VTT - WebVTT (.vtt)<br>iOS only supports VTT, Android supports all 3
|
||||
uri | URL for the text track. Currently, only tracks hosted on a webserver are supported
|
||||
|
||||
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
|
||||
@ -928,12 +870,12 @@ textTracks={[
|
||||
```
|
||||
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### trackId
|
||||
Configure an identifier for the video stream to link the playback context to the events emitted.
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### useTextureView
|
||||
Controls whether to output to a TextureView or SurfaceView.
|
||||
@ -947,7 +889,7 @@ useTextureView can only be set at same time you're setting the source.
|
||||
* **true (default)** - Use a TextureView
|
||||
* **false** - Use a SurfaceView
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### useSecureView
|
||||
Force the output to a SurfaceView and enables the secure surface.
|
||||
@ -959,7 +901,7 @@ SurfaceView is is the only one that can be labeled as secure.
|
||||
* **true** - Use security
|
||||
* **false (default)** - Do not use security
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### volume
|
||||
Adjust the volume.
|
||||
@ -989,7 +931,7 @@ Callback function that is called when the audio is about to become 'noisy' due t
|
||||
|
||||
Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onBandwidthUpdate
|
||||
Callback function that is called when the available bandwidth changes.
|
||||
@ -1007,9 +949,9 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Note: On Android ExoPlayer, you must set the [reportBandwidth](#reportbandwidth) prop to enable this event. This is due to the high volume of events generated.
|
||||
Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to enable this event. This is due to the high volume of events generated.
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### onBuffer
|
||||
Callback function that is called when the player buffers.
|
||||
@ -1027,7 +969,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onEnd
|
||||
Callback function that is called when the player reaches the end of the media.
|
||||
@ -1059,28 +1001,28 @@ Callback function that is called when the player is about to enter fullscreen mo
|
||||
|
||||
Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onFullscreenPlayerDidPresent
|
||||
Callback function that is called when the player has entered fullscreen mode.
|
||||
|
||||
Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onFullscreenPlayerWillDismiss
|
||||
Callback function that is called when the player is about to exit fullscreen mode.
|
||||
|
||||
Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onFullscreenPlayerDidDismiss
|
||||
Callback function that is called when the player has exited fullscreen mode.
|
||||
|
||||
Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### onLoad
|
||||
Callback function that is called when the media is loaded and ready to play.
|
||||
@ -1169,7 +1111,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
Platforms: Android
|
||||
|
||||
#### onReadyForDisplay
|
||||
Callback function that is called when the first video frame is ready for display. This is when the poster is removed.
|
||||
@ -1177,10 +1119,9 @@ Callback function that is called when the first video frame is ready for display
|
||||
Payload: none
|
||||
|
||||
* iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
|
||||
* Android: [MEDIA_INFO_VIDEO_RENDERING_START](https://developer.android.com/reference/android/media/MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START)
|
||||
* Android ExoPlayer [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
|
||||
* Android [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Web
|
||||
Platforms: Android, iOS, Web
|
||||
|
||||
#### onPictureInPictureStatusChanged
|
||||
Callback function that is called when picture in picture becomes active or inactive.
|
||||
@ -1256,7 +1197,7 @@ Example:
|
||||
Both the currentTime & seekTime are reported because the video player may not seek to the exact requested position in order to improve seek performance.
|
||||
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
|
||||
Platforms: Android, iOS, Windows UWP
|
||||
|
||||
#### onRestoreUserInterfaceForPictureInPictureStop
|
||||
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
|
||||
@ -1285,9 +1226,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
Support for timed metadata on Android MediaPlayer is limited at best and only compatible with some videos. It requires a target SDK of 23 or higher.
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
### Methods
|
||||
Methods operate on a ref to the Video element. You can create a ref using code like:
|
||||
@ -1308,7 +1247,7 @@ Example:
|
||||
this.player.dismissFullscreenPlayer();
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### presentFullscreenPlayer
|
||||
`presentFullscreenPlayer()`
|
||||
@ -1317,14 +1256,14 @@ Put the player in fullscreen mode.
|
||||
|
||||
On iOS, this displays the video in a fullscreen view controller with controls.
|
||||
|
||||
On Android ExoPlayer & MediaPlayer, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
|
||||
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
|
||||
|
||||
Example:
|
||||
```
|
||||
this.player.presentFullscreenPlayer();
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
Platforms: Android, iOS
|
||||
|
||||
#### save
|
||||
`save(): Promise`
|
||||
@ -1539,7 +1478,7 @@ android {
|
||||
```
|
||||
|
||||
#### ExoPlayer no longer detaches
|
||||
When using a router like the react-navigation TabNavigator, switching between tab routes would previously cause ExoPlayer to detach causing the video player to pause. We now don't detach the view, allowing the video to continue playing in a background tab. This matches the behavior for iOS. Android MediaPlayer will crash if it detaches when switching routes, so its behavior has not been changed.
|
||||
When using a router like the react-navigation TabNavigator, switching between tab routes would previously cause ExoPlayer to detach causing the video player to pause. We now don't detach the view, allowing the video to continue playing in a background tab. This matches the behavior for iOS.
|
||||
|
||||
#### useTextureView now defaults to true
|
||||
The SurfaceView, which ExoPlayer has been using by default has a number of quirks that people are unaware of and often cause issues. This includes not supporting animations or scaling. It also causes strange behavior if you overlay two videos on top of each other, because the SurfaceView will [punch a hole](https://developer.android.com/reference/android/view/SurfaceView) through other views. Since TextureView doesn't have these issues and behaves in the way most developers expect, it makes sense to make it the default.
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,8 +1,18 @@
|
||||
## Changelog
|
||||
|
||||
### Version 6.0.0-alpha1
|
||||
### Version 6.0.0-alpha.2
|
||||
|
||||
- Fix Exoplayer progress not reported when paused [#2664](https://github.com/react-native-video/react-native-video/pull/2664)
|
||||
|
||||
### Version 6.0.0-alpha.1
|
||||
|
||||
- Remove Android MediaPlayer support [#2724](https://github.com/react-native-video/react-native-video/pull/2724)
|
||||
- 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)
|
||||
@ -39,6 +49,7 @@
|
||||
- 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
|
||||
|
||||
|
1
LICENSE
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
|
||||
|
5
Video.js
5
Video.js
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native';
|
||||
import { ViewPropTypes } from 'deprecated-react-native-prop-types';
|
||||
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';
|
||||
@ -426,7 +426,7 @@ Video.propTypes = {
|
||||
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,
|
||||
@ -473,7 +473,6 @@ Video.propTypes = {
|
||||
bufferForPlaybackAfterRebufferMs: PropTypes.number,
|
||||
maxHeapAllocationPercent: PropTypes.number,
|
||||
}),
|
||||
stereoPan: PropTypes.number,
|
||||
rate: PropTypes.number,
|
||||
pictureInPicture: PropTypes.bool,
|
||||
playInBackground: PropTypes.bool,
|
||||
|
@ -1,50 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
android {
|
||||
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', 21)
|
||||
targetSdkVersion safeExtGet('targetSdkVersion', 28)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
// Remove this repository line after google releases to google() or mavenCentral()
|
||||
maven { url "https://dl.google.com/android/maven2" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.17.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('com.google.android.exoplayer:extension-okhttp:2.17.1') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
}
|
||||
implementation 'com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}'
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -8,19 +8,43 @@ android {
|
||||
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', 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.github.thang2162:Android-ScalableVideoView:v1.1.1'
|
||||
repositories {
|
||||
// Remove this repository line after google releases to google() or mavenCentral()
|
||||
maven { url "https://dl.google.com/android/maven2" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.17.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('com.google.android.exoplayer:extension-okhttp:2.17.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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,28 @@
|
||||
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();
|
||||
@ -23,8 +33,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,796 +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 boolean mPreventsDisplaySleepDuringVideoPlayback = true;
|
||||
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.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 && mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
// 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 setPreventsDisplaySleepDuringVideoPlaybackModifier(final boolean preventsDisplaySleepDuringVideoPlayback) {
|
||||
mPreventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback;
|
||||
|
||||
if (!mMediaPlayerValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMediaPlayer.setScreenOnWhilePlaying(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
setKeepScreenOn(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
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);
|
||||
setPreventsDisplaySleepDuringVideoPlaybackModifier(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
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;
|
||||
mMediaPlayer.seekTo(msec, MediaPlayer.SEEK_CLOSEST);
|
||||
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(mPreventsDisplaySleepDuringVideoPlayback);
|
||||
}
|
||||
|
||||
@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,172 +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_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK = "preventsDisplaySleepDuringVideoPlayback";
|
||||
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_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK)
|
||||
public void setPropPreventsDisplaySleepDuringVideoPlayback(final ReactVideoView videoView, final boolean doPreventSleep) {
|
||||
videoView.setPreventsDisplaySleepDuringVideoPlaybackModifier(doPreventSleep);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
280
dom/RCTVideo.js
280
dom/RCTVideo.js
@ -1,280 +0,0 @@
|
||||
// @flow
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
import { 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
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,5 +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'
|
||||
|
@ -21,7 +21,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _localSourceEncryptionKeyScheme:String?
|
||||
|
||||
/* Required to publish events */
|
||||
private var _eventDispatcher:RCTEventDispatcher?
|
||||
private var _eventDispatcher:RCTEventDispatcherProtocol?
|
||||
private var _videoLoadStarted:Bool = false
|
||||
|
||||
private var _pendingSeek:Bool = false
|
||||
@ -95,7 +95,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc var onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock?
|
||||
@objc var onGetLicense: RCTDirectEventBlock?
|
||||
|
||||
init(eventDispatcher:RCTEventDispatcher!) {
|
||||
init(eventDispatcher:RCTEventDispatcherProtocol!) {
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
|
||||
_eventDispatcher = eventDispatcher
|
||||
@ -1043,6 +1043,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
item.seek(to: CMTime.zero)
|
||||
self.applyModifiers()
|
||||
} else {
|
||||
self.setPaused(true);
|
||||
_playerObserver.removePlayerTimeObserver()
|
||||
}
|
||||
}
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-video",
|
||||
"version": "6.0.0-alpha.0",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"description": "A <Video /> element for react-native",
|
||||
"main": "Video.js",
|
||||
"license": "MIT",
|
||||
@ -13,26 +13,20 @@
|
||||
"@react-native-community/eslint-config": "^0.0.5",
|
||||
"eslint": "^6.5.1",
|
||||
"react": "16.9.0",
|
||||
"react-dom": "16.9.0",
|
||||
"react-hot-loader": "^4.12.19",
|
||||
"react-native": "0.61.5",
|
||||
"react-native-dom": "^0.5.0",
|
||||
"react-native-windows": "^0.61.0-0"
|
||||
},
|
||||
"dependencies": {
|
||||
"deprecated-react-native-prop-types": "^2.2.0",
|
||||
"keymirror": "^0.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"shaka-player": "^3.3.2"
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "yarn eslint .",
|
||||
"xbasic": "yarn --cwd examples/basic"
|
||||
},
|
||||
"files": [
|
||||
"android-exoplayer",
|
||||
"android",
|
||||
"dom",
|
||||
"ios",
|
||||
"windows",
|
||||
"FilterType.js",
|
||||
|
Loading…
Reference in New Issue
Block a user