Skip to content

Commit

Permalink
feat(ios): Add ios support for accessing WebVTT Subtitle Content (#3541)
Browse files Browse the repository at this point in the history
* feature: add support to get subtitle content data

* refactor: return a string of the subtitles

Push the parsing/formatting to the consumer side.

* chore: add types for new subtitle feature

* chore: run swiftlint and swiftformat

* chore: add documentation for new onSubtitleTracks callback

* chore: added test uri; basic implementation of feature; hotfix onTextTracks

added optional chaining for `return x?.selected` because tracks that don't have a track selected either by default or manually will return undefined and this can cause an error.

* feat: rename onSubtitleTracks to onTextTrackDataChanged

Renamed the onSubtitleTracks event to onTextTrackDataChanged across the codebase to clearly indicate the callback's purpose: being called when the text track's data changes. This change is reflected in the events documentation, example usage in VideoPlayer.tsx, and the relevant iOS implementation files for consistency and clarity, in line with PR feedback.

* chore: omit target property

target could be confusing for users so we have removed it. using the delete operator instead of using {target,...eventData} as that would give an eslint error about unused vars.
  • Loading branch information
coofzilla authored Feb 29, 2024
1 parent 4d4b56c commit 253ffb5
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 5 deletions.
19 changes: 18 additions & 1 deletion docs/pages/component/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,24 @@ Example:
}
```

Platforms: Android
### `onTextTrackDataChanged`
Callback function that is called when new subtitle data is available. It provides the actual subtitle content for the current selected text track, if available (mainly WebVTT).

Payload:

Property | Type | Description
--- | --- | ---
`subtitleTracks` | `string` | The subtitles text content in a compatible format.


Example:
```javascript
{
subtitleTracks: "This blade has a dark past.",
}
```

Platforms: iOS

### `onVideoTracks`
Callback function that is called when video tracks change
Expand Down
16 changes: 14 additions & 2 deletions examples/basic/src/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import Video, {
ResizeMode,
SelectedTrack,
DRMType,
OnTextTrackDataChangedData,
SelectedTrackType,
} from 'react-native-video';
import ToggleControl from './ToggleControl';
import MultiValueControl, {
Expand Down Expand Up @@ -120,7 +122,12 @@ class VideoPlayer extends Component {
},
];

srcIosList = [];
srcIosList = [
{
description: 'sintel with subtitles',
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
},
];

srcAndroidList = [
{
Expand Down Expand Up @@ -231,7 +238,7 @@ class VideoPlayer extends Component {

onTextTracks = (data: OnTextTracksData) => {
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
return x.selected;
return x?.selected;
});

this.setState({
Expand All @@ -248,6 +255,10 @@ class VideoPlayer extends Component {
}
};

onTextTrackDataChanged = (data: OnTextTrackDataChangedData) => {
console.log(`Subtitles: ${JSON.stringify(data, null, 2)}`);
};

onAspectRatio = (data: OnVideoAspectRatioData) => {
console.log('onAspectRadio called ' + JSON.stringify(data));
this.setState({
Expand Down Expand Up @@ -749,6 +760,7 @@ class VideoPlayer extends Component {
onLoad={this.onLoad}
onAudioTracks={this.onAudioTracks}
onTextTracks={this.onTextTracks}
onTextTrackDataChanged={this.onTextTrackDataChanged}
onProgress={this.onProgress}
onEnd={this.onEnd}
progressUpdateInterval={1000}
Expand Down
14 changes: 13 additions & 1 deletion ios/Video/Features/RCTPlayerObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange<Bool>)
func handleViewControllerOverlayViewFrameChange(overlayView: UIView, change: NSKeyValueObservedChange<CGRect>)
func handleTracksChange(playerItem: AVPlayerItem, change: NSKeyValueObservedChange<[AVPlayerItemTrack]>)
func handleLegibleOutput(strings: [NSAttributedString])
}

// MARK: - RCTPlayerObserver

class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate {
class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate, AVPlayerItemLegibleOutputPushDelegate {
weak var _handlers: RCTPlayerObserverHandler?

var player: AVPlayer? {
Expand All @@ -57,8 +58,11 @@ class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate {

// handle timedMetadata
let metadataOutput = AVPlayerItemMetadataOutput()
let legibleOutput = AVPlayerItemLegibleOutput()
playerItem.add(metadataOutput)
playerItem.add(legibleOutput)
metadataOutput.setDelegate(self, queue: .main)
legibleOutput.setDelegate(self, queue: .main)
}
}

Expand Down Expand Up @@ -113,6 +117,14 @@ class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate {
}
}

func legibleOutput(_: AVPlayerItemLegibleOutput,
didOutputAttributedStrings strings: [NSAttributedString],
nativeSampleBuffers _: [Any],
forItemTime _: CMTime) {
guard let _handlers else { return }
_handlers.handleLegibleOutput(strings: strings)
}

func addPlayerObservers() {
guard let player, let _handlers else {
return
Expand Down
7 changes: 7 additions & 0 deletions ios/Video/RCTVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc var onReceiveAdEvent: RCTDirectEventBlock?
@objc var onTextTracks: RCTDirectEventBlock?
@objc var onAudioTracks: RCTDirectEventBlock?
@objc var onTextTrackDataChanged: RCTDirectEventBlock?

@objc
func _onPictureInPictureStatusChanged() {
Expand Down Expand Up @@ -1388,4 +1389,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self.onAudioTracks?(["audioTracks": audioTracks])
}
}

func handleLegibleOutput(strings: [NSAttributedString]) {
if let subtitles = strings.first {
self.onTextTrackDataChanged?(["subtitleTracks": subtitles.string])
}
}
}
1 change: 1 addition & 0 deletions ios/Video/RCTVideoManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTextTracks, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onAudioTracks, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTextTrackDataChanged, RCTDirectEventBlock);

RCT_EXTERN_METHOD(save
: (NSDictionary*)options reactTag
Expand Down
14 changes: 14 additions & 0 deletions src/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
OnProgressData,
OnReceiveAdEventData,
OnSeekData,
OnTextTrackDataChangedData,
OnTextTracksData,
OnTimedMetadataData,
OnVideoAspectRatioData,
Expand Down Expand Up @@ -93,6 +94,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onTimedMetadata,
onAudioTracks,
onTextTracks,
onTextTrackDataChanged,
onVideoTracks,
onAspectRatio,
...rest
Expand Down Expand Up @@ -333,6 +335,17 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
[onTextTracks],
);

const _onTextTrackDataChanged = useCallback(
(
e: NativeSyntheticEvent<OnTextTrackDataChangedData & {target?: number}>,
) => {
const {...eventData} = e.nativeEvent;
delete eventData.target;
onTextTrackDataChanged?.(eventData as OnTextTrackDataChangedData);
},
[onTextTrackDataChanged],
);

const _onVideoTracks = useCallback(
(e: NativeSyntheticEvent<OnVideoTracksData>) => {
onVideoTracks?.(e.nativeEvent);
Expand Down Expand Up @@ -509,6 +522,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onTimedMetadata={_onTimedMetadata}
onAudioTracks={_onAudioTracks}
onTextTracks={_onTextTracks}
onTextTrackDataChanged={_onTextTrackDataChanged}
onVideoTracks={_onVideoTracks}
onVideoFullscreenPlayerDidDismiss={onFullscreenPlayerDidDismiss}
onVideoFullscreenPlayerDidPresent={onFullscreenPlayerDidPresent}
Expand Down
10 changes: 9 additions & 1 deletion src/VideoNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {NativeModules, requireNativeComponent} from 'react-native';
import type ResizeMode from './types/ResizeMode';
import type FilterType from './types/FilterType';
import type Orientation from './types/Orientation';
import type {AdEvent, EnumValues, OnTextTracksTypeData} from './types';
import type {
AdEvent,
EnumValues,
OnTextTrackDataChangedData,
OnTextTracksTypeData,
} from './types';

// -------- There are types for native component (future codegen) --------
// if you are looking for types for react component, see src/types/video.ts
Expand Down Expand Up @@ -366,6 +371,9 @@ export interface VideoNativeProps extends ViewProps {
onTimedMetadata?: (event: NativeSyntheticEvent<OnTimedMetadataData>) => void; // ios, android
onAudioTracks?: (event: NativeSyntheticEvent<OnAudioTracksData>) => void; // android
onTextTracks?: (event: NativeSyntheticEvent<OnTextTracksData>) => void; // android
onTextTrackDataChanged?: (
event: NativeSyntheticEvent<OnTextTrackDataChangedData>,
) => void; // iOS
onVideoTracks?: (event: NativeSyntheticEvent<OnVideoTracksData>) => void; // android
}

Expand Down
5 changes: 5 additions & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export type OnTextTracksData = Readonly<{
textTracks: ReadonlyArray<TextTrack>;
}>;

export type OnTextTrackDataChangedData = Readonly<{
subtitleTracks: string;
}>;

export type OnVideoTracksData = Readonly<{
videoTracks: ReadonlyArray<
Readonly<{
Expand Down Expand Up @@ -181,6 +185,7 @@ export interface ReactVideoEvents {
onTimedMetadata?: (e: OnTimedMetadataData) => void; //Android, iOS
onAudioTracks?: (e: OnAudioTracksData) => void; // Android
onTextTracks?: (e: OnTextTracksData) => void; //Android
onTextTrackDataChanged?: (e: OnTextTrackDataChangedData) => void; // iOS
onVideoTracks?: (e: OnVideoTracksData) => void; //Android
onAspectRatio?: (e: OnVideoAspectRatioData) => void;
}

0 comments on commit 253ffb5

Please sign in to comment.