Added onTimedMetadata callback for iOS player and Exoplayer (#487)

* added listener for timedMetadata event.

* added callback in RCTVideo for the timed metadata

* exposing onTimedMetadata to JS

* added forgotten method declaration

* returning array of string values

* added metadata type to the array

* added onMetadata method

* overridden onMetadata method on exoplayer2

* added format of return value from onMetadata

* added function reference in README file
This commit is contained in:
Andrea Cresta 2017-02-14 03:38:02 +01:00 committed by Matt Apperson
parent 07ac819a46
commit d792427ce1
8 changed files with 124 additions and 18 deletions

View File

@ -135,22 +135,23 @@ using System.Collections.Generic;
<Video source={{uri: "background"}} // Can be a URL or a local file.
ref={(ref) => {
this.player = ref
}} // Store reference
rate={1.0} // 0 is paused, 1 is normal.
volume={1.0} // 0 is muted, 1 is normal.
muted={false} // Mutes the audio entirely.
paused={false} // Pauses playback entirely.
resizeMode="cover" // Fill the whole screen at aspect ratio.
repeat={true} // Repeat forever.
playInBackground={false} // Audio continues to play when app entering background.
playWhenInactive={false} // [iOS] Video continues to play when control or notification center are shown.
progressUpdateInterval={250.0} // [iOS] Interval to fire onProgress (default to ~250ms)
onLoadStart={this.loadStart} // Callback when video starts to load
onLoad={this.setDuration} // Callback when video loads
onProgress={this.setTime} // Callback every ~250ms with currentTime
onEnd={this.onEnd} // Callback when playback finishes
onError={this.videoError} // Callback when video cannot be loaded
onBuffer={this.onBuffer} // Callback when remote video is buffering
}} // Store reference
rate={1.0} // 0 is paused, 1 is normal.
volume={1.0} // 0 is muted, 1 is normal.
muted={false} // Mutes the audio entirely.
paused={false} // Pauses playback entirely.
resizeMode="cover" // Fill the whole screen at aspect ratio.
repeat={true} // Repeat forever.
playInBackground={false} // Audio continues to play when app entering background.
playWhenInactive={false} // [iOS] Video continues to play when control or notification center are shown.
progressUpdateInterval={250.0} // [iOS] Interval to fire onProgress (default to ~250ms)
onLoadStart={this.loadStart} // Callback when video starts to load
onLoad={this.setDuration} // Callback when video loads
onProgress={this.setTime} // Callback every ~250ms with currentTime
onEnd={this.onEnd} // Callback when playback finishes
onError={this.videoError} // Callback when video cannot be loaded
onBuffer={this.onBuffer} // Callback when remote video is buffering
onTimedMetadata={this.onTimedMetadata} // Callback when the stream receive some metadata
style={styles.backgroundVideo} />
// Later to trigger fullscreen

View File

@ -79,6 +79,12 @@ export default class Video extends Component {
}
};
_onTimedMetadata = (event) => {
if (this.props.onTimedMetadata) {
this.props.onTimedMetadata(event.nativeEvent);
}
};
_onFullscreenPlayerWillPresent = (event) => {
if (this.props.onFullscreenPlayerWillPresent) {
this.props.onFullscreenPlayerWillPresent(event.nativeEvent);
@ -191,6 +197,7 @@ export default class Video extends Component {
onVideoSeek: this._onSeek,
onVideoEnd: this._onEnd,
onVideoBuffer: this._onBuffer,
onTimedMetadata: this._onTimedMetadata,
onVideoFullscreenPlayerWillPresent: this._onFullscreenPlayerWillPresent,
onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent,
onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss,
@ -248,6 +255,7 @@ Video.propTypes = {
onVideoProgress: PropTypes.func,
onVideoSeek: PropTypes.func,
onVideoEnd: PropTypes.func,
onTimedMetadata: PropTypes.func,
onVideoFullscreenPlayerWillPresent: PropTypes.func,
onVideoFullscreenPlayerDidPresent: PropTypes.func,
onVideoFullscreenPlayerWillDismiss: PropTypes.func,

View File

@ -4,6 +4,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.SurfaceView;
import android.view.TextureView;
@ -16,6 +17,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
@ -96,6 +99,7 @@ public final class ExoPlayerView extends FrameLayout {
this.player.setVideoListener(null);
this.player.removeListener(componentListener);
this.player.setVideoSurface(null);
this.player.setMetadataOutput(componentListener);
}
this.player = player;
shutterView.setVisibility(VISIBLE);
@ -108,6 +112,7 @@ public final class ExoPlayerView extends FrameLayout {
player.setVideoListener(componentListener);
player.addListener(componentListener);
player.setTextOutput(componentListener);
player.setMetadataOutput(componentListener);
}
}
@ -161,7 +166,7 @@ public final class ExoPlayerView extends FrameLayout {
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener {
TextRenderer.Output, ExoPlayer.EventListener, MetadataRenderer.Output {
// TextRenderer.Output implementation
@ -220,6 +225,10 @@ public final class ExoPlayerView extends FrameLayout {
updateForCurrentTrackSelections();
}
@Override
public void onMetadata(Metadata metadata) {
Log.d("onMetadata", "onMetadata");
}
}
}

View File

@ -26,6 +26,8 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@ -49,11 +51,14 @@ import java.net.CookieManager;
import java.net.CookiePolicy;
@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
LifecycleEventListener,
ExoPlayer.EventListener,
BecomingNoisyListener,
AudioManager.OnAudioFocusChangeListener {
AudioManager.OnAudioFocusChangeListener,
MetadataRenderer.Output
{
private static final String TAG = "ReactExoplayerView";
@ -192,6 +197,7 @@ class ReactExoplayerView extends FrameLayout implements
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, new DefaultLoadControl());
player.addListener(this);
player.setMetadataOutput(this);
exoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
@ -247,6 +253,7 @@ class ReactExoplayerView extends FrameLayout implements
playerPosition = player.getCurrentPosition();
}
player.release();
player.setMetadataOutput(null);
player = null;
trackSelector = null;
}
@ -480,6 +487,11 @@ class ReactExoplayerView extends FrameLayout implements
playerNeedsSource = true;
}
@Override
public void onMetadata(Metadata metadata) {
eventEmitter.timedMetadata(metadata);
}
// ReactExoplayerViewManager public api
public void setSrc(final Uri uri, final String extension) {

View File

@ -5,11 +5,18 @@ import android.view.View;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.TxxxFrame;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
class VideoEventEmitter {
@ -32,6 +39,7 @@ class VideoEventEmitter {
private static final String EVENT_READY = "onReadyForDisplay";
private static final String EVENT_BUFFER = "onVideoBuffer";
private static final String EVENT_IDLE = "onVideoIdle";
private static final String EVENT_TIMED_METADATA = "onTimedMetadata";
private static final String EVENT_AUDIO_BECOMING_NOISY = "onAudioBecomingNoisy";
private static final String EVENT_AUDIO_FOCUS_CHANGE = "onAudioFocusChanged";
@ -47,6 +55,7 @@ class VideoEventEmitter {
EVENT_READY,
EVENT_BUFFER,
EVENT_IDLE,
EVENT_TIMED_METADATA,
EVENT_AUDIO_BECOMING_NOISY,
EVENT_AUDIO_FOCUS_CHANGE,
};
@ -64,6 +73,7 @@ class VideoEventEmitter {
EVENT_READY,
EVENT_BUFFER,
EVENT_IDLE,
EVENT_TIMED_METADATA,
EVENT_AUDIO_BECOMING_NOISY,
EVENT_AUDIO_FOCUS_CHANGE,
})
@ -92,6 +102,9 @@ class VideoEventEmitter {
private static final String EVENT_PROP_ERROR_STRING = "errorString";
private static final String EVENT_PROP_ERROR_EXCEPTION = "";
private static final String EVENT_PROP_TIMED_METADATA = "metadata";
void setViewId(int viewId) {
this.viewId = viewId;
}
@ -168,6 +181,36 @@ class VideoEventEmitter {
receiveEvent(EVENT_ERROR, event);
}
void timedMetadata(Metadata metadata) {
WritableArray metadataArray = Arguments.createArray();
for (int i = 0; i < metadata.length(); i++) {
Id3Frame frame = (Id3Frame) metadata.get(i);
String value = "";
if (frame instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) frame;
value = txxxFrame.value;
}
String identifier = frame.id;
WritableMap map = Arguments.createMap();
map.putString("identifier", identifier);
map.putString("value", value);
metadataArray.pushMap(map);
}
WritableMap event = Arguments.createMap();
event.putArray(EVENT_PROP_TIMED_METADATA, metadataArray);
receiveEvent(EVENT_TIMED_METADATA, event);
}
void audioFocusChanged(boolean hasFocus) {
WritableMap map = Arguments.createMap();
map.putBoolean(EVENT_PROP_HAS_AUDIO_FOCUS, hasFocus);

View File

@ -16,6 +16,7 @@
@property (nonatomic, copy) RCTBubblingEventBlock onVideoProgress;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoSeek;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoEnd;
@property (nonatomic, copy) RCTBubblingEventBlock onTimedMetadata;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillPresent;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerDidPresent;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillDismiss;

View File

@ -9,6 +9,7 @@ static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"
static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty";
static NSString *const readyForDisplayKeyPath = @"readyForDisplay";
static NSString *const playbackRate = @"rate";
static NSString *const timedMetadata = @"timedMetadata";
@implementation RCTVideo
{
@ -226,6 +227,7 @@ static NSString *const playbackRate = @"rate";
[_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil];
[_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil];
_playerItemObserversSet = YES;
}
@ -238,6 +240,7 @@ static NSString *const playbackRate = @"rate";
[_playerItem removeObserver:self forKeyPath:statusKeyPath];
[_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath];
[_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath];
[_playerItem removeObserver:self forKeyPath:timedMetadata];
_playerItemObserversSet = NO;
}
}
@ -318,6 +321,34 @@ static NSString *const playbackRate = @"rate";
{
if (object == _playerItem) {
// When timeMetadata is read the event onTimedMetadata is triggered
if ([keyPath isEqualToString: timedMetadata])
{
NSArray<AVMetadataItem *> *items = [change objectForKey:@"new"];
if (items && ![items isEqual:[NSNull null]] && items.count > 0) {
NSMutableArray *array = [NSMutableArray new];
for (AVMetadataItem *item in items) {
NSString *value = item.value;
NSString *identifier = item.identifier;
if (![value isEqual: [NSNull null]]) {
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]];
[array addObject:dictionary];
}
}
self.onTimedMetadata(@{
@"target": self.reactTag,
@"metadata": array
});
}
}
if ([keyPath isEqualToString:statusKeyPath]) {
// Handle player item status change.
if (_playerItem.status == AVPlayerItemStatusReadyToPlay) {

View File

@ -41,6 +41,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillPresent, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTBubblingEventBlock);