Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TIMOB-25640] (7_0_X) iOS: Fix Ti.Media.VideoPlayer "playbackMode" constants #9715

Merged
merged 4 commits into from Jan 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions apidoc/Titanium/Media/Media.yml
Expand Up @@ -1358,11 +1358,19 @@ properties:
type: Number
permission: read-only
platforms: [android, iphone, ipad]
deprecated:
since: "7.0.0"
removed: "7.0.0"
notes: "This constant has been removed on iOS by using the official Apple AVPlayer that does not expose this functionality so far."
- name: VIDEO_PLAYBACK_STATE_SEEKING_FORWARD
summary: Video playback is seeking forward.
type: Number
permission: read-only
platforms: [android, iphone, ipad]
deprecated:
since: "7.0.0"
removed: "7.0.0"
notes: "This constant has been removed on iOS by using the official Apple AVPlayer that does not expose this functionality so far."
- name: VIDEO_PLAYBACK_STATE_STOPPED
summary: Video playback is stopped.
type: Number
Expand Down
2 changes: 0 additions & 2 deletions iphone/Classes/MediaModule.h
Expand Up @@ -201,8 +201,6 @@
@property (nonatomic, readonly) NSNumber *VIDEO_PLAYBACK_STATE_PLAYING;
@property (nonatomic, readonly) NSNumber *VIDEO_PLAYBACK_STATE_PAUSED;
@property (nonatomic, readonly) NSNumber *VIDEO_PLAYBACK_STATE_INTERRUPTED;
@property (nonatomic, readonly) NSNumber *VIDEO_PLAYBACK_STATE_SEEKING_FORWARD;
@property (nonatomic, readonly) NSNumber *VIDEO_PLAYBACK_STATE_SEEKING_BACKWARD;

@property (nonatomic, readonly) NSNumber *VIDEO_LOAD_STATE_UNKNOWN;
@property (nonatomic, readonly) NSNumber *VIDEO_LOAD_STATE_PLAYABLE;
Expand Down
6 changes: 6 additions & 0 deletions iphone/Classes/MediaModule.m
Expand Up @@ -264,6 +264,12 @@ - (NSString *)apiName

#endif

// Constants for VideoPlayer.playbackState
MAKE_SYSTEM_PROP(VIDEO_PLAYBACK_STATE_INTERRUPTED, TiVideoPlayerPlaybackStateInterrupted);
MAKE_SYSTEM_PROP(VIDEO_PLAYBACK_STATE_PAUSED, TiVideoPlayerPlaybackStatePaused);
MAKE_SYSTEM_PROP(VIDEO_PLAYBACK_STATE_PLAYING, TiVideoPlayerPlaybackStatePlaying);
MAKE_SYSTEM_PROP(VIDEO_PLAYBACK_STATE_STOPPED, TiVideoPlayerPlaybackStateStopped);

//Constants for Camera
#if defined(USE_TI_MEDIACAMERA_FRONT) || defined(USE_TI_MEDIACAMERA_REAR) || defined(USE_TI_MEDIACAMERA_FLASH_OFF) || defined(USE_TI_MEDIACAMERA_FLASH_AUTO) || defined(USE_TI_MEDIACAMERA_FLASH_ON)
MAKE_SYSTEM_PROP(CAMERA_FRONT, UIImagePickerControllerCameraDeviceFront);
Expand Down
13 changes: 13 additions & 0 deletions iphone/Classes/TiMediaVideoPlayerProxy.h
Expand Up @@ -23,6 +23,16 @@ typedef NS_ENUM(NSInteger, VideoRepeatMode) {
VideoRepeatModeOne,
};

typedef NS_ENUM(NSInteger, TiVideoPlayerPlaybackState) {
TiVideoPlayerPlaybackStateUnknown = -1,
TiVideoPlayerPlaybackStateStopped,
TiVideoPlayerPlaybackStatePlaying,
TiVideoPlayerPlaybackStatePaused,
TiVideoPlayerPlaybackStateInterrupted,
TiVideoPlayerPlaybackStateSeekingForward, // Not supported so far
TiVideoPlayerPlaybackStateSeekingBackward, // Not supported so far
};

@interface TiMediaVideoPlayerProxy : TiViewProxy {
@protected
AVPlayerViewController *movie;
Expand Down Expand Up @@ -53,6 +63,9 @@ typedef NS_ENUM(NSInteger, VideoRepeatMode) {

// Have to track loading in the proxy in addition to the view, in case we load before the view should be rendered
BOOL loaded;

// Track the playback state for parity
TiVideoPlayerPlaybackState _playbackState;
}

@property (nonatomic, readwrite, assign) id url;
Expand Down
112 changes: 86 additions & 26 deletions iphone/Classes/TiMediaVideoPlayerProxy.m
Expand Up @@ -96,25 +96,32 @@ - (void)addNotificationObserver
WARN_IF_BACKGROUND_THREAD; //NSNotificationCenter is not threadsafe!
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

// For durationavailable
// For durationavailable event
[movie addObserver:self forKeyPath:@"player.currentItem.duration" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

// For playbackstate
[movie addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// The AVPlayer does not properly support state management on iOS < 10.
// Remove this once we bump the minimum iOS version to 10+.
if ([TiUtils isIOS10OrGreater]) {
// iOS 10+: For playbackState property / playbackstate event
[movie addObserver:self forKeyPath:@"player.timeControlStatus" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:self];
} else {
// iOS < 10: For playbackstate event
[movie addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

// For playing
[self addObserver:self forKeyPath:@"url" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

// For load / loadstate / preload
[movie addObserver:self forKeyPath:@"player.status" options:0 context:nil];

// naturalSize
// For naturalSize event
[movie addObserver:self forKeyPath:@"videoBounds" options:NSKeyValueObservingOptionInitial context:nil];

// For complete
// For complete event
[nc addObserver:self selector:@selector(handlePlayerNotification:) name:AVPlayerItemDidPlayToEndTimeNotification object:[[movie player] currentItem]];

// For error
// For error event
[nc addObserver:self selector:@selector(handlePlayerErrorNotification:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:[[movie player] currentItem]];
}

Expand All @@ -123,11 +130,16 @@ - (void)removeNotificationObserver
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

[movie removeObserver:self forKeyPath:@"player.currentItem.duration"];
[movie removeObserver:self forKeyPath:@"player.rate"];
[self removeObserver:self forKeyPath:@"url"];
[movie removeObserver:self forKeyPath:@"player.status"];
[movie removeObserver:self forKeyPath:@"videoBounds"];

if ([TiUtils isIOS10OrGreater]) {
[movie removeObserver:self forKeyPath:@"player.timeControlStatus"];
} else {
[movie removeObserver:self forKeyPath:@"player.rate"];
}

[nc removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[nc removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:nil];
}
Expand Down Expand Up @@ -671,10 +683,11 @@ - (void)setSourceType:(id)type

- (NSNumber *)playbackState
{
if ([movie player] != nil) {
return NUMINT([[movie player] rate]);
if (_playbackState != TiVideoPlayerPlaybackStateUnknown) {
return NUMINTEGER(_playbackState);
}
return NUMINT(AVPlayerStatusUnknown);

return NUMINTEGER(TiVideoPlayerPlaybackStateStopped);
}

- (void)setRepeatMode:(id)value
Expand Down Expand Up @@ -707,7 +720,9 @@ - (TiColor *)backgroundColor
- (void)stop:(id)args
{
ENSURE_UI_THREAD(stop, args);

playing = NO;

[[movie player] seekToTime:CMTimeMake(0, 1)];
[[movie player] pause];
}
Expand Down Expand Up @@ -825,6 +840,8 @@ - (void)handlePlayerNotification:(NSNotification *)notification
- (void)handlePlayerErrorNotification:(NSNotification *)note
{
NSError *error = note.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey];
_playbackState = TiVideoPlayerPlaybackStateInterrupted;

if ([self _hasListeners:@"error"]) {
NSDictionary *event = [NSDictionary dictionaryWithObject:[error localizedDescription] forKey:@"error"];
[self fireEvent:@"error" withObject:event];
Expand Down Expand Up @@ -890,40 +907,77 @@ - (void)handleLoadStateChangeNotification:(NSNotification *)note

- (void)handleNowPlayingNotification:(NSNotification *)note
{
_playbackState = TiVideoPlayerPlaybackStatePlaying;

if ([self _hasListeners:@"playing"]) {
NSDictionary *event = [NSDictionary dictionaryWithObject:[self url] forKey:@"url"];
[self fireEvent:@"playing" withObject:event];
}
}

- (void)handlePlaybackStateChangeNotification:(NSNotification *)note
- (void)handleNaturalSizeAvailableNotification:(NSNotification *)note
{
if ([self _hasListeners:@"playbackstate"]) {
NSDictionary *event = [NSDictionary dictionaryWithObject:[self playbackState] forKey:@"playbackState"];
[self fireEvent:@"playbackstate" withObject:event];
if ([self _hasListeners:@"naturalsizeavailable"]) {
[self fireEvent:@"naturalsizeavailable"
withObject:@{
@"naturalSize" : @{
@"width" : NUMFLOAT(movie.videoBounds.size.width),
@"height" : NUMFLOAT(movie.videoBounds.size.height)
}
}];
}
}

// iOS < 10
- (void)handlePlaybackStateChangeNotification:(NSNotification *)note
{
TiVideoPlayerPlaybackState oldState = _playbackState;

switch ([[movie player] status]) {
case AVPlayerStatusUnknown:
case AVPlayerStatusFailed:
playing = NO;
_playbackState = TiVideoPlayerPlaybackStateInterrupted;
break;
case AVPlayerStatusReadyToPlay:
playing = ([[movie player] rate] == 1.0);
if (playing) {
_playbackState = TiVideoPlayerPlaybackStatePlaying;
} else if (movie.player.currentItem.duration.value == movie.player.currentItem.currentTime.value || !movie.player.currentItem.canStepBackward) {
_playbackState = TiVideoPlayerPlaybackStateStopped;
} else {
_playbackState = TiVideoPlayerPlaybackStatePaused;
}
break;
}

if ([self _hasListeners:@"playbackstate"] && oldState != _playbackState) {
NSDictionary *event = [NSDictionary dictionaryWithObject:[self playbackState] forKey:@"playbackState"];
[self fireEvent:@"playbackstate" withObject:event];
}
}

- (void)handleNaturalSizeAvailableNotification:(NSNotification *)note
// iOS 10+
- (void)handleTimeControlStatusNotification:(NSNotification *)note
{
if ([self _hasListeners:@"naturalsizeavailable"]) {
[self fireEvent:@"naturalsizeavailable"
withObject:@{
@"naturalSize" : @{
@"width" : NUMFLOAT(movie.videoBounds.size.width),
@"height" : NUMFLOAT(movie.videoBounds.size.height)
}
}];
TiVideoPlayerPlaybackState oldState = _playbackState;
playing = movie.player.timeControlStatus == AVPlayerTimeControlStatusPlaying;

if (movie.player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
_playbackState = TiVideoPlayerPlaybackStatePlaying;
} else if (movie.player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
if (movie.player.currentItem.duration.value == movie.player.currentItem.currentTime.value) {
_playbackState = TiVideoPlayerPlaybackStateStopped;
} else {
_playbackState = TiVideoPlayerPlaybackStatePaused;
}
} else if ([TiUtils boolValue:[loadProperties valueForKey:@"autoplay"]]) {
_playbackState = TiVideoPlayerPlaybackStatePlaying;
}

if ([self _hasListeners:@"playbackstate"] && oldState != _playbackState) {
NSDictionary *event = [NSDictionary dictionaryWithObject:NUMINTEGER(_playbackState) forKey:@"playbackState"];
[self fireEvent:@"playbackstate" withObject:event];
}
}

Expand All @@ -932,9 +986,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
if ([keyPath isEqualToString:@"player.currentItem.duration"]) {
[self handleDurationAvailableNotification:nil];
}
if ([keyPath isEqualToString:@"player.rate"]) {
[self handlePlaybackStateChangeNotification:nil];
}
if ([keyPath isEqualToString:@"url"]) {
[self handleNowPlayingNotification:nil];
}
Expand All @@ -944,6 +995,15 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
if ([keyPath isEqualToString:@"videoBounds"]) {
[self handleNaturalSizeAvailableNotification:nil];
}
if ([TiUtils isIOS10OrGreater]) {
if ([keyPath isEqualToString:@"player.timeControlStatus"]) {
[self handleTimeControlStatusNotification:nil];
}
} else {
if ([keyPath isEqualToString:@"player.rate"]) {
[self handlePlaybackStateChangeNotification:nil];
}
}
}

@end
Expand Down
57 changes: 14 additions & 43 deletions tests/Resources/ti.media.videoplayer.addontest.js
Expand Up @@ -9,47 +9,18 @@
/* eslint no-unused-expressions: "off" */
'use strict';
var should = require('./utilities/assertions'),
utilities = require('./utilities/utilities');

describe('Titanium.Media.VideoPlayer', function() {
it.ios('Close window containing a video player (TIMOB-25574)', function() {
var win = Titanium.UI.createWindow();

var nav = Titanium.UI.iOS.createNavigationWindow({
window: win
});

var detailWindow = Titanium.UI.createWindow();

var videoPlayer = Titanium.Media.createVideoPlayer({
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
top: 2,
autoplay: true,
backgroundColor: 'blue',
height: 300,
width: 300,
mediaControlStyle: Titanium.Media.VIDEO_CONTROL_DEFAULT,
scalingMode: Titanium.Media.VIDEO_SCALING_ASPECT_FIT
});

// When the first window openes, open the next one
win.addEventListener('open', function() {
this.timeout(500);
nav.openWindow(detailWindow);
});

// Once the next window opens, close it again
detailWindow.addEventListener('open', function() {
this.timeout(500);
nav.closeWindow(detailWindow);
});

// If the detail window closes successfully without a crahs, we are good!
detailWindow.addEventListener('close', function() {
finish();
});

detailWindow.add(videoPlayer);
nav.open();
});
utilities = require('./utilities/utilities');

describe('Titanium.Media.VideoPlayer', function () {
it.windowsMissing('VIDEO_PLAYBACK_* constants', function () {
should(Ti.Media.VIDEO_PLAYBACK_STATE_STOPPED).eql(0);
should(Ti.Media.VIDEO_PLAYBACK_STATE_PLAYING).eql(1);
should(Ti.Media.VIDEO_PLAYBACK_STATE_PAUSED).eql(2);
should(Ti.Media.VIDEO_PLAYBACK_STATE_INTERRUPTED).eql(3);

if (utilities.isAndroid()) {
should(Ti.Media.VIDEO_PLAYBACK_STATE_SEEKING_FORWARD).eql(4);
should(Ti.Media.VIDEO_PLAYBACK_STATE_SEEKING_BACKWARD).eql(5);
}
});
});