Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feat] Added additional buttons for contextual actions
Browse files Browse the repository at this point in the history
Eduard Mazur authored and Eduard Mazur committed Jan 14, 2025
1 parent 6cc1bff commit d398e14
Showing 9 changed files with 191 additions and 13 deletions.
20 changes: 20 additions & 0 deletions ios/Video/DataStructures/ContextualActionData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// ContextualActionData.swift
// react-native-video
//
// Created by Eduard Mazur on 14.01.2025.
//

import Foundation

struct ContextualActionData {
let action: String
let startAt: Double
let endAt: Double?
}

public enum ContextualButtonState {
case none
case skipIntro
case nextEpisode
}
88 changes: 88 additions & 0 deletions ios/Video/RCTVideo+ContextualActions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// RCTVideo+ContextualActions.swift
// react-native-video
//
// Created by Eduard Mazur on 14.01.2025.
//

import AVKit

@available(tvOS 15.0, *)
extension RCTVideo {
func configureContextualActions() {
guard let player = playerForContextualActions else {
print("Player is not initialized.")
return
}

let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))

timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] currentTime in
self?.updateContextualActions(currentTime: currentTime)
}
}

func removeContextualActionsTimeObserver() {
guard let player = playerForContextualActions, let token = timeObserverToken else { return }
player.removeTimeObserver(token)
timeObserverToken = nil
}

func updateContextualActions(currentTime: CMTime) {
let currentSeconds = CMTimeGetSeconds(currentTime)

guard let playerViewController = playerViewControllerForContextualActions else {
return
}

Check failure on line 37 in ios/Video/RCTVideo+ContextualActions.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
for actionData in contextualActionData {
if currentSeconds >= actionData.startAt,
let endAt = actionData.endAt, currentSeconds < endAt {
handleAction(actionData, playerViewController: playerViewController)
return
} else if currentSeconds >= actionData.startAt, actionData.endAt == nil {
handleAction(actionData, playerViewController: playerViewController)
return
}
}

if currentContextualState != .none {
playerViewController.contextualActions = []
currentContextualState = .none
}
}

func handleAction(_ actionData: ContextualActionData, playerViewController: AVPlayerViewController) {
switch actionData.action {
case "SkipIntro":
if currentContextualState != .skipIntro {
let skipIntroAction = UIAction(title: "Skip Intro") { [weak self] _ in
self?.handleSkipIntro()
}
playerViewController.contextualActions = [skipIntroAction]
currentContextualState = .skipIntro
}
case "NextEpisode":
if currentContextualState != .nextEpisode {
let nextEpisodeAction = UIAction(title: "Next Episode") { [weak self] _ in
self?.handleNextEpisode()
}
playerViewController.contextualActions = [nextEpisodeAction]
currentContextualState = .nextEpisode
}
default:
break
}
}

Check failure on line 77 in ios/Video/RCTVideo+ContextualActions.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func handleSkipIntro() {
onSkipIntro?(["message": "Skip Intro triggered"])
let skipTime = CMTime(seconds: 120, preferredTimescale: 1)
playerForContextualActions?.seek(to: skipTime, toleranceBefore: .zero, toleranceAfter: .zero)
}

func handleNextEpisode() {
onNextEpisode?(["message": "Next Episode triggered"])
print("Next episode triggered")
}
}
2 changes: 1 addition & 1 deletion ios/Video/RCTVideo-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import "RCTVideoSwiftLog.h"
#import <React/RCTViewManager.h>

#import <React/RCTEventDispatcher.h>
#if __has_include(<react-native-video/RCTVideoCache.h>)
#import "RCTVideoCache.h"
#endif
54 changes: 52 additions & 2 deletions ios/Video/RCTVideo.swift
Original file line number Diff line number Diff line change
@@ -135,7 +135,42 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc var onTextTracks: RCTDirectEventBlock?
@objc var onAudioTracks: RCTDirectEventBlock?
@objc var onTextTrackDataChanged: RCTDirectEventBlock?
@objc var onSkipIntro: RCTDirectEventBlock?
@objc var onNextEpisode: RCTDirectEventBlock?

Check failure on line 140 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// Contextual Actions
public var playerViewControllerForContextualActions: AVPlayerViewController? {
return _playerViewController
}
public var playerForContextualActions: AVPlayer? {
return _player
}

Check failure on line 148 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public var currentContextualState: ContextualButtonState = .none
public var timeObserverToken: Any?

Check failure on line 151 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
var contextualActionData: [ContextualActionData] = []

Check failure on line 153 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
private func parseContextualActions() {
contextualActionData = contextualActions.compactMap { actionDict in
guard let action = actionDict["action"] as? String,
let startAt = actionDict["startAt"] as? Double else {
return nil
}

let endAt = actionDict["endAt"] as? Double
return ContextualActionData(action: action, startAt: startAt, endAt: endAt)
}
}

Check failure on line 165 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
@objc var contextualActions: [[String: Any]] = [] {
didSet {
parseContextualActions()
}
}

Check failure on line 171 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)

Check failure on line 172 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)

Check failure on line 173 in ios/Video/RCTVideo.swift

GitHub Actions / Swift-Lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
@objc
func _onPictureInPictureEnter() {
onPictureInPictureStatusChanged?(["isActive": NSNumber(value: true)])
@@ -289,6 +324,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_pip = nil
#endif
ReactNativeVideoManager.shared.unregisterView(newInstance: self)

#if os(tvOS)
if #available(tvOS 15.0, *) {
removeContextualActionsTimeObserver()
}
#endif
}

// MARK: - App lifecycle handlers
@@ -1104,11 +1145,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
if _controls {
let viewController: UIViewController! = self.reactViewController()
viewController?.addChild(_playerViewController)

self.addSubview(_playerViewController.view)
}

_playerObserver.playerViewController = _playerViewController

if #available(tvOS 15.0, *) {
configureContextualActions()
}
}


func createPlayerViewController(player: AVPlayer, withPlayerItem _: AVPlayerItem) -> RCTVideoPlayerViewController {
let viewController = RCTVideoPlayerViewController()
@@ -1717,21 +1764,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
}
}

@available(tvOS 14.0, *)
@objc
func enterPictureInPicture() {
#if os(iOS)
if _pip?._pipController == nil {
initPictureinPicture()
_playerViewController?.allowsPictureInPicturePlayback = true
}
_pip?.enterPictureInPicture()
#endif
}

@objc
func exitPictureInPicture() {
guard isPictureInPictureActive() else { return }

_pip?.exitPictureInPicture()
#if os(iOS)
_pip?.exitPictureInPicture()
if _enterPictureInPictureOnLeave {
initPictureinPicture()
_playerViewController?.allowsPictureInPicturePlayback = true
4 changes: 4 additions & 0 deletions ios/Video/RCTVideoManager.m
Original file line number Diff line number Diff line change
@@ -87,4 +87,8 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager)
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject)

// Should support contextual actions
RCT_EXPORT_VIEW_PROPERTY(contextualActions, NSArray);
RCT_EXPORT_VIEW_PROPERTY(onSkipIntro, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onNextEpisode, RCTDirectEventBlock);
@end
1 change: 1 addition & 0 deletions ios/Video/RCTVideoManager.swift
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ class RCTVideoManager: RCTViewManager {
})
}

@available(tvOS 14.0, *)
@objc(enterPictureInPictureCmd:)
func enterPictureInPictureCmd(_ reactTag: NSNumber) {
performOnVideoView(withReactTag: reactTag, callback: { videoView in
22 changes: 12 additions & 10 deletions ios/Video/ReactNativeVideoManager.swift
Original file line number Diff line number Diff line change
@@ -6,31 +6,33 @@
import Foundation

public class ReactNativeVideoManager: RNVPlugin {
private let expectedMaxVideoCount = 2
private let expectedMaxVideoCount = 10

// create a private initializer
private init() {}

public static let shared: ReactNativeVideoManager = .init()

private var instanceCount = 0
private var pluginList: [RNVPlugin] = Array()
var instanceList: [RCTVideo] = Array()
var pluginList: [RNVPlugin] = Array()

/**
* register a new view
* register a new ReactExoplayerViewManager in the managed list
*/
func registerView(newInstance _: RCTVideo) {
if instanceCount > expectedMaxVideoCount {
func registerView(newInstance: RCTVideo) {
if instanceList.count > expectedMaxVideoCount {
DebugLog("multiple Video displayed ?")
}
instanceCount += 1
instanceList.append(newInstance)
}

/**
* unregister existing view
* unregister existing ReactExoplayerViewManager in the managed list
*/
func unregisterView(newInstance _: RCTVideo) {
instanceCount -= 1
func unregisterView(newInstance: RCTVideo) {
if let i = instanceList.firstIndex(of: newInstance) {
instanceList.remove(at: i)
}
}

/**
2 changes: 2 additions & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
@@ -267,4 +267,6 @@ export interface ReactVideoEvents {
onTextTrackDataChanged?: (e: OnTextTrackDataChangedData) => void; // iOS
onVideoTracks?: (e: OnVideoTracksData) => void; //Android
onAspectRatio?: (e: OnVideoAspectRatioData) => void;
onSkipIntro?: () => void; // The logic for skipping the intro is in the Swift file no need to pass seek or onSeek
onNextEpisode?: () => void;
}
11 changes: 11 additions & 0 deletions src/types/video.ts
Original file line number Diff line number Diff line change
@@ -274,6 +274,16 @@ export type ControlsStyles = {
liveLabel?: string;
};

export declare enum ContextualAction {
SKIP_INTRO = 'SkipIntro',
NEXT_EPISODE = 'NextEpisode',
}
export type ContextualActions = {
action: ContextualAction;
startAt?: number;
endAt?: number;
}

Check failure on line 285 in src/types/video.ts

GitHub Actions / Lint JS (eslint, prettier)

Insert `;`

export interface ReactVideoRenderLoaderProps {
source?: ReactVideoSource;
style?: StyleProp<ImageStyle>;
@@ -349,4 +359,5 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
debug?: DebugConfig;
allowsExternalPlayback?: boolean; // iOS
controlsStyles?: ControlsStyles; // Android
contextualActions?: ContextualActions[];
}

0 comments on commit d398e14

Please sign in to comment.