Merge pull request #3216 from Duell10111/tvos-custom-playback-exerpience-fork
feat: allow customization of tvOS playback expierence
This commit is contained in:
commit
a0fa8312ba
31
API.md
31
API.md
@ -465,6 +465,19 @@ bufferConfig={{
|
||||
|
||||
Platforms: Android
|
||||
|
||||
#### chapters
|
||||
To provide a custom chapter source for tvOS. This prop takes an array of objects with the properties listed below.
|
||||
|
||||
| Property | Type | Description |
|
||||
|-----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| title | string | The title of the chapter to create |
|
||||
| startTime | number | The start time of the chapter in seconds |
|
||||
| endTime | number | The end time of the chapter in seconds |
|
||||
| uri | string? | Optional: Provide an http orl or the some base64 string to override the image of the chapter. For some media files the images are generated automatically |
|
||||
|
||||
|
||||
Platforms: tvOS
|
||||
|
||||
#### 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.
|
||||
|
||||
@ -964,6 +977,24 @@ source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
|
||||
|
||||
Platforms: iOS, Android
|
||||
|
||||
##### Overriding the metadata of a source
|
||||
|
||||
Provide an optional `title`, `subtitle` and/or `description` properties for the video.
|
||||
Useful when to adapt the tvOS playback experience.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
source={{
|
||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
title: 'Custom Title',
|
||||
subtitle: 'Custom Subtitle',
|
||||
description: 'Custom Description'
|
||||
}}
|
||||
```
|
||||
|
||||
Platforms: tvOS
|
||||
|
||||
#### subtitleStyle
|
||||
|
||||
Property | Description | Platforms
|
||||
|
13
Video.js
13
Video.js
@ -343,7 +343,11 @@ export default class Video extends Component {
|
||||
patchVer: source.patchVer || 0,
|
||||
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
|
||||
startTime: source.startTime || 0,
|
||||
endTime: source.endTime
|
||||
endTime: source.endTime,
|
||||
// Custom Metadata
|
||||
title: source.title,
|
||||
subtitle: source.subtitle,
|
||||
description: source.description,
|
||||
},
|
||||
onVideoLoadStart: this._onLoadStart,
|
||||
onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
|
||||
@ -492,6 +496,13 @@ Video.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
chapters: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
startTime: PropTypes.number.isRequired,
|
||||
endTime: PropTypes.number.isRequired,
|
||||
})
|
||||
),
|
||||
paused: PropTypes.bool,
|
||||
muted: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
|
File diff suppressed because one or more lines are too long
25
ios/Video/DataStructures/Chapter.swift
Normal file
25
ios/Video/DataStructures/Chapter.swift
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
struct Chapter {
|
||||
let title: String
|
||||
let uri: String?
|
||||
let startTime: Double
|
||||
let endTime: Double
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
init(_ json: NSDictionary!) {
|
||||
guard json != nil else {
|
||||
self.json = nil
|
||||
self.title = ""
|
||||
self.uri = nil
|
||||
self.startTime = 0
|
||||
self.endTime = 0
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
self.title = json["title"] as? String ?? ""
|
||||
self.uri = json["uri"] as? String
|
||||
self.startTime = json["startTime"] as? Double ?? 0
|
||||
self.endTime = json["endTime"] as? Double ?? 0
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@ struct VideoSource {
|
||||
let requestHeaders: Dictionary<String,Any>?
|
||||
let startTime: Int64?
|
||||
let endTime: Int64?
|
||||
// Custom Metadata
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let description: String?
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
@ -22,6 +26,9 @@ struct VideoSource {
|
||||
self.requestHeaders = nil
|
||||
self.startTime = nil
|
||||
self.endTime = nil
|
||||
self.title = nil
|
||||
self.subtitle = nil
|
||||
self.description = nil
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
@ -33,5 +40,8 @@ struct VideoSource {
|
||||
self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any>
|
||||
self.startTime = json["startTime"] as? Int64
|
||||
self.endTime = json["endTime"] as? Int64
|
||||
self.title = json["title"] as? String
|
||||
self.subtitle = json["subtitle"] as? String
|
||||
self.description = json["description"] as? String
|
||||
}
|
||||
}
|
||||
|
49
ios/Video/Features/RCTVideoTVUtils.swift
Normal file
49
ios/Video/Features/RCTVideoTVUtils.swift
Normal file
@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
|
||||
/*!
|
||||
* Collection of helper functions for tvOS specific features
|
||||
*/
|
||||
|
||||
#if os(tvOS)
|
||||
enum RCTVideoTVUtils {
|
||||
static func makeNavigationMarkerGroups(_ chapters: [Chapter]) -> [AVNavigationMarkersGroup] {
|
||||
var metadataGroups = [AVTimedMetadataGroup]()
|
||||
|
||||
// Iterate over the defined chapters and build a timed metadata group object for each.
|
||||
chapters.forEach { chapter in
|
||||
metadataGroups.append(makeTimedMetadataGroup(for: chapter))
|
||||
}
|
||||
|
||||
return [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metadataGroups)]
|
||||
}
|
||||
|
||||
static func makeTimedMetadataGroup(for chapter: Chapter) -> AVTimedMetadataGroup {
|
||||
var metadata = [AVMetadataItem]()
|
||||
|
||||
// Create a metadata item that contains the chapter title.
|
||||
let titleItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierTitle, value: chapter.title)
|
||||
metadata.append(titleItem)
|
||||
|
||||
// Create a time range for the metadata group.
|
||||
let timescale: Int32 = 600
|
||||
let startTime = CMTime(seconds: chapter.startTime, preferredTimescale: timescale)
|
||||
let endTime = CMTime(seconds: chapter.endTime, preferredTimescale: timescale)
|
||||
let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime)
|
||||
|
||||
// Image
|
||||
if let imgUri = chapter.uri,
|
||||
let uri = URL(string: imgUri),
|
||||
let imgData = try? Data(contentsOf: uri),
|
||||
let image = UIImage(data: imgData),
|
||||
let pngData = image.pngData()
|
||||
{
|
||||
let imageItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierArtwork, value: pngData)
|
||||
metadata.append(imageItem)
|
||||
}
|
||||
|
||||
return AVTimedMetadataGroup(items: metadata, timeRange: timeRange)
|
||||
}
|
||||
}
|
||||
#endif
|
@ -316,4 +316,18 @@ enum RCTVideoUtils {
|
||||
}
|
||||
return (asset, assetOptions)
|
||||
}
|
||||
|
||||
static func createMetadataItems(for mapping: [AVMetadataIdentifier: Any]) -> [AVMetadataItem] {
|
||||
return mapping.compactMap { createMetadataItem(for:$0, value:$1) }
|
||||
}
|
||||
|
||||
static func createMetadataItem(for identifier: AVMetadataIdentifier,
|
||||
value: Any) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.identifier = identifier
|
||||
item.value = value as? NSCopying & NSObjectProtocol
|
||||
// Specify "und" to indicate an undefined language.
|
||||
item.extendedLanguageTag = "und"
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _source:VideoSource?
|
||||
private var _playerBufferEmpty:Bool = true
|
||||
private var _playerLayer:AVPlayerLayer?
|
||||
private var _chapters:[Chapter]?
|
||||
|
||||
private var _playerViewController:RCTVideoPlayerViewController?
|
||||
private var _videoURL:NSURL?
|
||||
@ -369,7 +370,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?) -> AVPlayerItem {
|
||||
if (_textTracks == nil) || _textTracks?.count==0 {
|
||||
return AVPlayerItem(asset: asset)
|
||||
return self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
}
|
||||
|
||||
// AVPlayer can't airplay AVMutableCompositions
|
||||
@ -384,7 +385,35 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
setTextTracks(validTextTracks)
|
||||
}
|
||||
|
||||
return AVPlayerItem(asset: mixComposition)
|
||||
return self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
|
||||
}
|
||||
|
||||
func playerItemPropegateMetadata(_ playerItem: AVPlayerItem!) -> AVPlayerItem {
|
||||
var mapping: [AVMetadataIdentifier: Any] = [:]
|
||||
|
||||
if let title = _source?.title {
|
||||
mapping[.commonIdentifierTitle] = title
|
||||
}
|
||||
|
||||
if let subtitle = _source?.subtitle {
|
||||
mapping[.iTunesMetadataTrackSubTitle] = subtitle
|
||||
}
|
||||
|
||||
if let description = _source?.description {
|
||||
mapping[.commonIdentifierDescription] = description
|
||||
}
|
||||
|
||||
if #available(iOS 12.2, *), !mapping.isEmpty {
|
||||
playerItem.externalMetadata = RCTVideoUtils.createMetadataItems(for: mapping)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
if let chapters = _chapters {
|
||||
playerItem.navigationMarkerGroups = RCTVideoTVUtils.makeNavigationMarkerGroups(chapters)
|
||||
}
|
||||
#endif
|
||||
|
||||
return playerItem
|
||||
}
|
||||
|
||||
// MARK: - Prop setters
|
||||
@ -675,6 +704,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)}
|
||||
}
|
||||
|
||||
@objc
|
||||
func setChapters(_ chapters:[NSDictionary]?) {
|
||||
setChapters(chapters?.map { Chapter($0) })
|
||||
}
|
||||
|
||||
func setChapters(_ chapters:[Chapter]?) {
|
||||
_chapters = chapters
|
||||
}
|
||||
|
||||
@objc
|
||||
func setFullscreen(_ fullscreen:Bool) {
|
||||
if fullscreen && !_fullscreenPlayerPresented && _player != nil {
|
||||
|
@ -14,6 +14,7 @@ RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(textTracks, NSArray);
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectedAudioTrack, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(chapters, NSArray);
|
||||
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);
|
||||
|
@ -41,5 +41,6 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
|
||||
return orientation
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user