From 777d59f88d3dd71ee789da4ef6faeb8a9dcd58ba Mon Sep 17 00:00:00 2001 From: TakayukiHoshi1984 Date: Fri, 13 Oct 2017 17:45:21 +0900 Subject: [PATCH 01/13] =?UTF-8?q?Host=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=81=AEMediaPlayer=E9=96=A2=E9=80=A3=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=B9=E3=81=AE=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 32 + .../dConnectDeviceHost/Classes/DPHostUtils.h | 1 + .../DPHostMediaPlayerProfile.m | 1310 ++--------------- .../Players/DPHostIPodAudioPlayer.h | 14 + .../Players/DPHostIPodAudioPlayer.m | 288 ++++ .../Players/DPHostMediaPlayer.h | 70 + .../Players/DPHostMediaPlayer.m | 99 ++ .../Players/DPHostMediaPlayerFactory.h | 26 + .../Players/DPHostMediaPlayerFactory.m | 364 +++++ .../Players/DPHostMoviePlayer.h | 15 + .../Players/DPHostMoviePlayer.m | 433 ++++++ .../profile/DPHostNotificationProfile.m | 2 +- 12 files changed, 1469 insertions(+), 1185 deletions(-) create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj index 8701cac3..fabadc21 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj @@ -62,6 +62,10 @@ 97FE1A21192B6DA400808FA5 /* DPHostSystemProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 97FE1A20192B6DA400808FA5 /* DPHostSystemProfile.m */; }; AB3B2C281BCF311000C5E3A2 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB3B2C261BCF311000C5E3A2 /* AVFoundation.framework */; }; AB3B2C291BCF311000C5E3A2 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB3B2C271BCF311000C5E3A2 /* AVKit.framework */; }; + AB912A791F8F174B002ABE36 /* DPHostMediaPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = AB912A781F8F174B002ABE36 /* DPHostMediaPlayer.m */; }; + AB912A7C1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = AB912A7B1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m */; }; + AB912A7F1F8F4092002ABE36 /* DPHostMoviePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = AB912A7E1F8F4092002ABE36 /* DPHostMoviePlayer.m */; }; + AB912A821F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = AB912A811F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.m */; }; ABA43AC41EE562ED0019FE35 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABA43AC31EE562ED0019FE35 /* UserNotifications.framework */; }; ABDA3C6D1A0C58F3001F30A0 /* DPHostConnectionProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */; }; ABDA3C701A0C743A001F30A0 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABDA3C6F1A0C743A001F30A0 /* CoreBluetooth.framework */; }; @@ -151,6 +155,14 @@ 97FE1A20192B6DA400808FA5 /* DPHostSystemProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPHostSystemProfile.m; sourceTree = ""; }; AB3B2C261BCF311000C5E3A2 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; AB3B2C271BCF311000C5E3A2 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; + AB912A771F8F174B002ABE36 /* DPHostMediaPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostMediaPlayer.h; sourceTree = ""; }; + AB912A781F8F174B002ABE36 /* DPHostMediaPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostMediaPlayer.m; sourceTree = ""; }; + AB912A7A1F8F3081002ABE36 /* DPHostIPodAudioPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostIPodAudioPlayer.h; sourceTree = ""; }; + AB912A7B1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostIPodAudioPlayer.m; sourceTree = ""; }; + AB912A7D1F8F4092002ABE36 /* DPHostMoviePlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostMoviePlayer.h; sourceTree = ""; }; + AB912A7E1F8F4092002ABE36 /* DPHostMoviePlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostMoviePlayer.m; sourceTree = ""; }; + AB912A801F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostMediaPlayerFactory.h; sourceTree = ""; }; + AB912A811F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostMediaPlayerFactory.m; sourceTree = ""; }; ABA43AC31EE562ED0019FE35 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; ABDA3C6B1A0C58F3001F30A0 /* DPHostConnectionProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DPHostConnectionProfile.h; sourceTree = ""; }; ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPHostConnectionProfile.m; sourceTree = ""; }; @@ -261,6 +273,7 @@ 25F0BC31199310C10095C333 /* DPHostMediaPlayer */ = { isa = PBXGroup; children = ( + AB912A761F8F13A4002ABE36 /* Players */, 25F0BC3F199343600095C333 /* DPHostMediaContext.h */, 25F0BC40199343600095C333 /* DPHostMediaContext.m */, 25E0296F1991BD3400AFC171 /* DPHostMediaPlayerProfile.h */, @@ -407,6 +420,21 @@ path = profile; sourceTree = ""; }; + AB912A761F8F13A4002ABE36 /* Players */ = { + isa = PBXGroup; + children = ( + AB912A771F8F174B002ABE36 /* DPHostMediaPlayer.h */, + AB912A781F8F174B002ABE36 /* DPHostMediaPlayer.m */, + AB912A7A1F8F3081002ABE36 /* DPHostIPodAudioPlayer.h */, + AB912A7B1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m */, + AB912A7D1F8F4092002ABE36 /* DPHostMoviePlayer.h */, + AB912A7E1F8F4092002ABE36 /* DPHostMoviePlayer.m */, + AB912A801F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.h */, + AB912A811F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.m */, + ); + path = Players; + sourceTree = ""; + }; D6384EAA1A7625CF00384167 /* DPHostCanvas */ = { isa = PBXGroup; children = ( @@ -633,6 +661,7 @@ EFA3EC4E1E45DD95007E559E /* DPHostGeolocationProfile.m in Sources */, 97FE19F2192B2FB400808FA5 /* DPHostDevicePlugin.m in Sources */, D6384EA91A7625BD00384167 /* DPHostCanvasProfile.m in Sources */, + AB912A7C1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m in Sources */, D6C860251D38D3AF00232EEF /* DPHostService.m in Sources */, D69997451A7E210D00EF934F /* DPHostCanvasView.m in Sources */, D6384EAD1A7625CF00384167 /* DPHostCanvasUIViewController.m in Sources */, @@ -640,8 +669,10 @@ 25FF1DB7196A9B8000EA7F72 /* DPHostNotificationProfile.m in Sources */, 25F0BC41199343600095C333 /* DPHostMediaContext.m in Sources */, 25FF1DB1196A89A700EA7F72 /* DPHostFileProfile.m in Sources */, + AB912A821F8F5AD9002ABE36 /* DPHostMediaPlayerFactory.m in Sources */, 25DF80D6196CCD7D003F6FF3 /* DPHostBatteryProfile.m in Sources */, 25FF1DBA196AA53400EA7F72 /* DPHostDeviceOrientationProfile.m in Sources */, + AB912A791F8F174B002ABE36 /* DPHostMediaPlayer.m in Sources */, EFDD96A71AB805F70068E984 /* DPHostTouchUIViewController.m in Sources */, EFDD96A11AB805980068E984 /* DPHostTouchProfile.m in Sources */, 25B42C6519865F1000B65E83 /* DPHostRecorderContext.m in Sources */, @@ -654,6 +685,7 @@ EFF228D01E3A05DF0006F100 /* DPHostDeviceRepeatExecutor.m in Sources */, EFF228D31E3A05DF0006F100 /* DPHostTimeoutSchedule.m in Sources */, EFF228D61E3A05DF0006F100 /* DPHostUtil.m in Sources */, + AB912A7F1F8F4092002ABE36 /* DPHostMoviePlayer.m in Sources */, D69997481A7F1AD800EF934F /* DPHostCanvasDrawObject.m in Sources */, 2585BB941980D89F00AAE43C /* DPHostSettingProfile.m in Sources */, ABDA3C6D1A0C58F3001F30A0 /* DPHostConnectionProfile.m in Sources */, diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h index ebc4463b..81947716 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h @@ -11,6 +11,7 @@ #define SELF_PLUGIN ((DPHostDevicePlugin *)self.plugin) #define WEAKSELF_PLUGIN ((DPHostDevicePlugin *)weakSelf.plugin) +#define SUPER_PLUGIN ((DPHostDevicePlugin *)super.plugin) @interface DPHostUtils : NSObject diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/DPHostMediaPlayerProfile.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/DPHostMediaPlayerProfile.m index 7ec2a9f4..a8ea1b39 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/DPHostMediaPlayerProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/DPHostMediaPlayerProfile.m @@ -7,85 +7,30 @@ // http://opensource.org/licenses/mit-license.php // -// MARK: ファイルシステムやWeb上のメディアコンテンツも参照できる様にする? -// MARK: UIScreenの+screensを使って、別ディスプレイで動画表示できると良いかも? - -#import -#import -#import -#import #import #import "DPHostDevicePlugin.h" #import "DPHostMediaPlayerProfile.h" #import "DPHostMediaContext.h" -#import "DPHostReachability.h" -#import "DPHostService.h" #import "DPHostUtils.h" +#import "DPHostMediaPlayer.h" +#import "DPHostMoviePlayer.h" +#import "DPHostMediaPlayerFactory.h" + @interface DPHostMediaPlayerProfile() /// @brief イベントマネージャ @property DConnectEventManager *eventMgr; -@property MediaPlayerType currentMediaPlayer; ///< 現在使われているメディアプレイヤー - -/// 動画再生用ビューコントローラー -@property MPMoviePlayerViewController *viewController; -/// iPodプレイヤー -@property MPMusicPlayerController *musicPlayer; -/// iPodプレイヤーのクエリー -@property MPMediaQuery *defaultMediaQuery; - -/// アセットライブラリ検索用のロック -@property NSObject *lockAssetsLibraryQuerying; -/// iPodライブラリ検索用のロック -@property NSObject *lockIPodLibraryQuerying; -@property NSString *nowPlayingMediaId; -@property ALAssetsLibrary *library; - --(void) nowPlayingItemChangedInIPod:(NSNotification *)notification; -- (void) nowPlayingItemChangedInMoviePlayer:(NSNotification *)notification; - -/** - @brief アセットライブラリ(カメラロール等) - にあるアセット群からメディアコンテキスト群を生成する。 - @param query 文字列クエリー - @param mimeType MIMEタイプに対するクエリー - @return カメラロールなどのアセットライブラリにあるメディア群 - */ -- (NSArray *)contextsBySearchingAssetsLibraryWithQuery:(NSString *)query - mimeType:(NSString *)mimeType; - -/** - @brief iPodライブラリにあるメディア群からメディアコンテキスト群を生成する。 - @param query 文字列クエリー - @param mimeType MIMEタイプに対するクエリー - @return iPodライブラリにあるメディア群 - */ -- (NSArray *)contextsBySearchingIPodLibraryWithQuery:(NSString *)query - mimeType:(NSString *)mimeType; - -- (MPMoviePlayerViewController *)viewControllerWithURL:(NSURL *)url; - -/** - @brief ムービープレイヤー(MPMoviePlayerViewController) - が表示されている場合はYESを返却する。 - @return MPMoviePlayerViewControllerが表示されているかどうか。 - */ -- (BOOL)moviePlayerViewControllerIsPresented; - -/** - MPMoviePlayerPlaybackDidFinishNotification通知の際に呼び出されるセレクター。 - @param[in] notification 受け取った通知 - */ -- (void) videoFinished:(NSNotification*)notification; +@property DPHostMediaPlayer *mediaPlayer; @end @implementation DPHostMediaPlayerProfile + - (instancetype)init { self = [super init]; @@ -95,65 +40,19 @@ - (instancetype)init // イベントマネージャを取得 self.eventMgr = [DConnectEventManager sharedManagerForClass:[DPHostDevicePlugin class]]; - // iPodプレイヤーを取得 - self.musicPlayer = [MPMusicPlayerController iPodMusicPlayer]; - self.musicPlayer.shuffleMode = MPMusicShuffleModeDefault; - self.musicPlayer.repeatMode = MPMusicRepeatModeDefault; - self.defaultMediaQuery = [MPMediaQuery songsQuery]; - [self.defaultMediaQuery addFilterPredicate: - [MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger:TargetMPMediaType] - forProperty:MPMediaItemPropertyMediaType]]; - self.library = [ALAssetsLibrary new]; - // 初期はiPodミュージックプレイヤーを設定しておく。 - _currentMediaPlayer = MediaPlayerTypeIPod; - + // API登録(didReceiveGetPlayStatusRequest相当) NSString *getPlayStatusRequestApiPath = [self apiPath: nil attributeName: DConnectMediaPlayerProfileAttrPlayStatus]; [self addGetPath: getPlayStatusRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - NSString *status; - switch ([weakSelf currentMediaPlayer]) { - case MediaPlayerTypeMoviePlayer: - // MoviePlayer - switch ([weakSelf viewController].moviePlayer.playbackState) { - case MPMoviePlaybackStateStopped: - status = DConnectMediaPlayerProfileStatusStop; - break; - case MPMoviePlaybackStatePlaying: - status = DConnectMediaPlayerProfileStatusPlay; - break; - case MPMoviePlaybackStatePaused: - status = DConnectMediaPlayerProfileStatusPause; - break; - default: - break; - } - break; - case MediaPlayerTypeIPod: - // iPodミュージックプレイヤー - switch ([weakSelf musicPlayer].playbackState) { - case MPMusicPlaybackStateStopped: - status = DConnectMediaPlayerProfileStatusStop; - break; - case MPMusicPlaybackStatePlaying: - status = DConnectMediaPlayerProfileStatusPlay; - break; - case MPMusicPlaybackStatePaused: - status = DConnectMediaPlayerProfileStatusPause; - break; - default: - break; - } - break; - } - - if (status) { + if ([weakSelf mediaPlayer]) { + NSString *status = [[weakSelf mediaPlayer] playStatus]; [DConnectMediaPlayerProfile setStatus:status target:response]; [response setResult:DConnectMessageResultTypeOk]; } else { - [response setErrorToUnknownWithMessage:@"Status is unknown."]; + [response setErrorToUnknownWithMessage:@"Status is unknown"]; } return YES; }]; @@ -191,132 +90,64 @@ - (instancetype)init NSString *query = [DConnectMediaPlayerProfile queryFromRequest:request]; NSString *mimeType = [DConnectMediaPlayerProfile mimeTypeFromRequest:request]; NSString *orderStr = [DConnectMediaPlayerProfile orderFromRequest:request]; - NSArray *order = nil; - if (orderStr) { - order = [orderStr componentsSeparatedByString:@","]; - } NSNumber *offset = [DConnectMediaPlayerProfile offsetFromRequest:request]; NSNumber *limit = [DConnectMediaPlayerProfile limitFromRequest:request]; - - NSString *sortTarget; - NSString *sortOrder; - [weakSelf checkOrder:&sortOrder sortTarget:&sortTarget response:response order:order]; - if ([response integerForKey:DConnectMessageResult] == DConnectMessageResultTypeError) { - return YES; - } - NSComparator comp; - [weakSelf compareOrderWithResponse:response sortTarget:sortTarget sortOrder:sortOrder comparator:&comp]; - if ([response integerForKey:DConnectMessageResult] == DConnectMessageResultTypeError) { - return YES; - } - - if (offset && offset.integerValue < 0) { - [response setErrorToInvalidRequestParameterWithMessage:@"offset must be a non-negative value."]; - return YES; - } - if (limit && limit.integerValue < 0) { - [response setErrorToInvalidRequestParameterWithMessage:@"limit must be a positive value."]; - return YES; - } - NSString *limitString = [request stringForKey:DConnectMediaPlayerProfileParamLimit]; - NSString *offsetString = [request stringForKey:DConnectMediaPlayerProfileParamOffset]; - if (![DPHostUtils existDigitWithString:limitString]) { - [response setErrorToInvalidRequestParameterWithMessage:@"limit must be a digit number."]; - return YES; - } - if (![DPHostUtils existDigitWithString:offsetString]) { - [response setErrorToInvalidRequestParameterWithMessage:@"offset must be a digit number."]; - return YES; - } - NSMutableArray *ctxArr = [NSMutableArray array]; - [ctxArr addObjectsFromArray:[weakSelf contextsBySearchingAssetsLibraryWithQuery:query mimeType:mimeType]]; - [ctxArr addObjectsFromArray:[weakSelf contextsBySearchingIPodLibraryWithQuery:query mimeType:mimeType]]; - - if (offset && offset.integerValue >= ctxArr.count) { - [response setErrorToInvalidRequestParameterWithMessage:@"offset exceeds the size of the media list."]; - return YES; - } - - // 並び替えを実行 - NSArray *tmpArr = [ctxArr sortedArrayUsingComparator:comp]; - - // ページングのために配列の一部分だけ抜き出し - if (offset || limit) { - NSUInteger offsetVal = offset ? offset.unsignedIntegerValue : 0; - NSUInteger limitVal = limit ? limit.unsignedIntegerValue : ctxArr.count; - tmpArr = [tmpArr subarrayWithRange: - NSMakeRange(offset.unsignedIntegerValue, - MIN(ctxArr.count - offsetVal, limitVal))]; - } - - [DConnectMediaPlayerProfile setCount:(int)tmpArr.count target:response]; - DConnectArray *media = [DConnectArray array]; - for (DPHostMediaContext *ctx in tmpArr) { - DConnectMessage *medium = [DConnectMessage message]; - [ctx setVariousMetadataToMessage:medium omitMediaId:NO]; - [media addMessage:medium]; - } - [DConnectMediaPlayerProfile setMedia:media target:response]; - - [response setResult:DConnectMessageResultTypeOk]; - - return YES; - }]; - - // API登録(didReceiveGetSeekRequest相当) - NSString *getSeekRequestApiPath = [self apiPath: nil - attributeName: DConnectMediaPlayerProfileAttrSeek]; - [self addGetPath: getSeekRequestApiPath - api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - - __block NSTimeInterval pos; - void(^block)(void) = nil; - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - block = ^{ - pos = _musicPlayer.currentPlaybackTime; - }; - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented;" - "please perform Media PUT API first to present the view controller."]; - return YES; + NSError *error = nil; + NSArray *tmpArr = [DPHostMediaPlayerFactory searchMediaWithQuery:query + mimeType:mimeType + order:orderStr + offset:offset + limit:limit + error:&error]; + if (!error) { + [DConnectMediaPlayerProfile setCount:(int)tmpArr.count target:response]; + DConnectArray *media = [DConnectArray array]; + for (DPHostMediaContext *ctx in tmpArr) { + DConnectMessage *medium = [DConnectMessage message]; + [ctx setVariousMetadataToMessage:medium omitMediaId:NO]; + [media addMessage:medium]; } - block = ^{ - pos = _viewController.moviePlayer.playableDuration; - }; - } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; - return YES; - } - if ([NSThread isMainThread]) { - block(); + [DConnectMediaPlayerProfile setMedia:media target:response]; + + [response setResult:DConnectMessageResultTypeOk]; } else { - dispatch_sync(dispatch_get_main_queue(), block); + [response setError:error.code message:error.localizedDescription]; } - [DConnectMediaPlayerProfile setPos:pos target:response]; - [response setResult:DConnectMessageResultTypeOk]; - return YES; }]; + + // API登録(didReceivePutMediaRequest相当) NSString *putMediaRequestApiPath = [self apiPath: nil attributeName: DConnectMediaPlayerProfileAttrMedia]; [self addPutPath: putMediaRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - @synchronized (weakSelf) { - NSString *serviceId = [request serviceId]; - NSString *mediaId = [DConnectMediaPlayerProfile mediaIdFromRequest:request]; - if (!mediaId) { - [response setErrorToInvalidRequestParameterWithMessage:@"mediaId must be specified."]; - return YES; + NSString *mediaId = [DConnectMediaPlayerProfile mediaIdFromRequest:request]; + if (!mediaId) { + [response setErrorToInvalidRequestParameterWithMessage:@"mediaId must be specified."]; + return YES; + } + NSError *error = nil; + if (weakSelf.mediaPlayer) { + // MoviePlayerViewControllerがすでに起動している場合は一度閉じる + if ([weakSelf.mediaPlayer isKindOfClass:[DPHostMoviePlayer class]]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [((DPHostMoviePlayer*)weakSelf.mediaPlayer) closeMoviePlayerViewController]; + }); + //ViewControllerが閉じるのを待つ + sleep(1.0); } - return [weakSelf putMediaRequest: request - response: response - serviceId: serviceId - mediaId: mediaId]; } + weakSelf.mediaPlayer = [DPHostMediaPlayerFactory createPlayerWithMediaId:[mediaId stringByReplacingOccurrencesOfString:@" " withString:@"%20"] + plugin:weakSelf.plugin + error:&error]; + if (weakSelf.mediaPlayer || !error) { + [response setResult:DConnectMessageResultTypeOk]; + } else { + [response setError:error.code message:error.localizedDescription]; + } + return YES; }]; // API登録(didReceivePutPlayRequest相当) @@ -324,99 +155,16 @@ - (instancetype)init attributeName: DConnectMediaPlayerProfileAttrPlay]; [self addPutPath: putPlayRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - @synchronized (weakSelf) { - NSString *serviceId = [request serviceId]; - - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - MPMediaItem *mediaItem = [weakSelf musicPlayer].nowPlayingItem; - if ((!mediaItem || _nowPlayingMediaId) && [_musicPlayer playbackState] == MPMusicPlaybackStateStopped) { - - [weakSelf putMediaRequest:request - response:response - serviceId:serviceId - mediaId:[weakSelf nowPlayingMediaId]]; - mediaItem = _musicPlayer.nowPlayingItem; - } - NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; - if (isCloudItem) { - DPHostReachability *networkReachability - = [DPHostReachability reachabilityForInternetConnection]; - NetworkStatus networkStatus = [networkReachability currentReachabilityStatus]; - if ([isCloudItem boolValue] && networkStatus == NotReachable) { - // iCloud上の音楽項目 - // (つまりiOSデバイス側にまだダウンロードされていない)で、 - // 尚かつインターネット接続が無い場合は - // 再生できない。 - [response setErrorToUnknownWithMessage: - @"Internet is not reachable; the specified audio media item " - "is an iClould item and its playback requires an Internet connection."]; - return YES; - } - } - void(^block)(void) = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusPlay; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMusicWithMessage:mediaPlayer]; - [[weakSelf musicPlayer] setCurrentPlaybackTime:0.0f]; - [[weakSelf musicPlayer] play]; - - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - // 現在再生中の曲が変わった時の通知 - [notificationCenter addObserver:weakSelf - selector:@selector(nowPlayingItemChangedInIPod:) - name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification - object:[weakSelf musicPlayer]]; - - // 通知開始 - [[weakSelf musicPlayer] beginGeneratingPlaybackNotifications]; - - }; - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - [response setResult:DConnectMessageResultTypeOk]; - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented;" - " please perform Media PUT API first to present the view controller."]; - } else { - if ([weakSelf viewController].moviePlayer.playbackState != MPMoviePlaybackStatePlaying) { - if ([weakSelf viewController].moviePlayer.contentURL) { - void(^block)(void) = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusPlay; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMovieWithMessage:mediaPlayer]; - [[weakSelf viewController].moviePlayer setCurrentPlaybackTime:0.0f]; - [[weakSelf viewController].moviePlayer play]; - [response setResult:DConnectMessageResultTypeOk]; - [[DConnectManager sharedManager] sendResponse:response]; - - }; - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - return NO; - } - [response setErrorToUnknownWithMessage: - @"Media cannot be played; media is not specified."]; - } else { - [response setErrorToUnknownWithMessage: - @"Media cannot be played; it is already playing."]; - } - } - } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; - } - + NSError *error = nil; + DPHostPlayerBlock block = nil; + if (weakSelf.mediaPlayer) { + block = [weakSelf.mediaPlayer playWithError:&error]; + } else { + [response setErrorToIllegalServerStateWithMessage:@"Please call PUT Media API first"]; return YES; } + [weakSelf runMediaPlayerForBlock:block error:error response:response]; + return YES; }]; // API登録(didReceivePutStopRequest相当) @@ -424,71 +172,16 @@ - (instancetype)init attributeName: DConnectMediaPlayerProfileAttrStop]; [self addPutPath: putStopRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - @synchronized(weakSelf) { - void(^block)(void) = nil; - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - if (_musicPlayer.playbackState == MPMusicPlaybackStateStopped) { - //[response setErrorToIllegalServerState]; - [response setResult:DConnectMessageResultTypeOk]; - return YES; - } - block = ^{ - // iTunes関連の通知の削除 - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - - [notificationCenter removeObserver:weakSelf - name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification - object:_musicPlayer]; - // 通知終了 - [_musicPlayer endGeneratingPlaybackNotifications]; - - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusStop; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMusicWithMessage:mediaPlayer]; - [[weakSelf musicPlayer] stop]; - [response setResult:DConnectMessageResultTypeOk]; - }; - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (_viewController.moviePlayer.playbackState == MPMoviePlaybackStateStopped) { - //[response setErrorToIllegalServerState]; - [response setResult:DConnectMessageResultTypeOk]; - return YES; - } - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented;" - "please perform Media PUT API first to present the view controller."]; - } else { - block = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusStop; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMovieWithMessage:mediaPlayer]; - // ムービープレイヤーを閉じる。 - [_viewController.moviePlayer stop]; - [_viewController dismissMoviePlayerViewControllerAnimated]; - [response setResult:DConnectMessageResultTypeOk]; - }; - } - } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; - return YES; - } - - if (block) { - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - // ViewControllerが閉じるのを待つ - [NSThread sleepForTimeInterval:0.5]; - - } - + NSError *error = nil; + DPHostPlayerBlock block = nil; + if (weakSelf.mediaPlayer) { + block = [weakSelf.mediaPlayer stopWithError:&error]; + } else { + [response setErrorToIllegalServerStateWithMessage:@"Player status is illegal."]; return YES; } + [weakSelf runMediaPlayerForBlock:block error:error response:response]; + return YES; }]; // API登録(didReceivePutPauseRequest相当) @@ -497,58 +190,15 @@ - (instancetype)init [self addPutPath: putPauseRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - void(^block)(void) = nil; - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - if ([_musicPlayer playbackState] == MPMusicPlaybackStatePlaying) { - block = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusPause; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [self sendEventMusicWithMessage:mediaPlayer]; - - - [_musicPlayer pause]; - [response setResult:DConnectMessageResultTypeOk]; - }; - } else { - [response setErrorToUnknownWithMessage:@"Media cannnot be paused; media is not playing."]; - return YES; - } - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented;" - " please perform Media PUT API first to present the view controller."]; - } else { - if (_viewController.moviePlayer.playbackState == MPMusicPlaybackStatePlaying) { - block = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusPause; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMovieWithMessage:mediaPlayer]; - - - [_viewController.moviePlayer pause]; - [response setResult:DConnectMessageResultTypeOk]; - }; - } else { - [response setErrorToUnknownWithMessage:@"Media cannnot be paused; media is not playing."]; - return YES; - } - } + NSError *error = nil; + DPHostPlayerBlock block = nil; + if (weakSelf.mediaPlayer) { + block = [weakSelf.mediaPlayer pauseWithError:&error]; } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; + [response setErrorToIllegalServerStateWithMessage:@"Player status is illegal."]; return YES; } - - if (block) { - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - } - + [weakSelf runMediaPlayerForBlock:block error:error response:response]; return YES; }]; @@ -558,81 +208,47 @@ - (instancetype)init [self addPutPath: putResumeRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - if ([_musicPlayer playbackState] == MPMusicPlaybackStatePaused) { - MPMediaItem *mediaItem = [weakSelf musicPlayer].nowPlayingItem; - NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; - if (isCloudItem) { - DPHostReachability *networkReachability - = [DPHostReachability reachabilityForInternetConnection]; - NetworkStatus networkStatus = [networkReachability currentReachabilityStatus]; - if ([isCloudItem boolValue] && networkStatus == NotReachable) { - // iCloud上の音楽項目 - //(つまりiOSデバイス側にまだダウンロードされていない)で、 - //尚かつインターネット接続が無い場合は - // 再生できない。 - [response setErrorToUnknownWithMessage: - @"Internet is not reachable;" - " the specified audio media item is an iClould " - "item and its playback requires an Internet connection."]; - return YES; - } - } - - void(^block)(void) = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusResume; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMusicWithMessage:mediaPlayer]; - - - [[weakSelf musicPlayer] play]; - [response setResult:DConnectMessageResultTypeOk]; - }; - if (block) { - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - } - - [response setResult:DConnectMessageResultTypeOk]; - } else { - [response setErrorToUnknownWithMessage:@"Media cannot be resumed; media is not paused."]; - } - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented;" - "please perform Media PUT API first to present the view controller."]; + NSError *error = nil; + DPHostPlayerBlock block = nil; + if (weakSelf.mediaPlayer) { + block = [weakSelf.mediaPlayer resumeWithError:&error]; + } else { + [response setErrorToIllegalServerStateWithMessage:@"Player status is illegal."]; + return YES; + } + [weakSelf runMediaPlayerForBlock:block error:error response:response]; + return YES; + }]; + // API登録(didReceiveGetSeekRequest相当) + NSString *getSeekRequestApiPath = [self apiPath: nil + attributeName: DConnectMediaPlayerProfileAttrSeek]; + [self addGetPath: getSeekRequestApiPath + api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { + + __block NSTimeInterval pos = 0.0f; + __block NSError *error = nil; + DPHostPlayerBlock block = nil; + if ([weakSelf mediaPlayer]) { + block = ^{ + pos = [[weakSelf mediaPlayer] seekStatusWithError:&error]; + }; + } else { + [response setErrorToIllegalServerStateWithMessage:@"Player status is illegal."]; + return YES; + } + if (!error) { + if ([NSThread isMainThread]) { + block(); } else { - if (_viewController.moviePlayer.playbackState == MPMoviePlaybackStatePaused) { - void(^block)(void) = ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusResume; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [weakSelf sendEventMovieWithMessage:mediaPlayer]; - - - [[weakSelf viewController].moviePlayer play]; - [response setResult:DConnectMessageResultTypeOk]; - }; - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - } else { - [response setErrorToUnknownWithMessage:@"Media cannot be resumed; media is not paused."]; - } + dispatch_sync(dispatch_get_main_queue(), block); } + [DConnectMediaPlayerProfile setPos:pos target:response]; + [response setResult:DConnectMessageResultTypeOk]; } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; + [response setError:error.code message:error.localizedDescription]; } return YES; }]; - // API登録(didReceivePutSeekRequest相当) NSString *putSeekRequestApiPath = [self apiPath: nil attributeName: DConnectMediaPlayerProfileAttrSeek]; @@ -647,45 +263,15 @@ - (instancetype)init return YES; } - void(^block)(void) = nil; - if (_currentMediaPlayer == MediaPlayerTypeIPod) { - MPMediaItem *nowPlayingItem = [weakSelf musicPlayer].nowPlayingItem; - NSNumber *playbackDuration = [nowPlayingItem valueForProperty:MPMediaItemPropertyPlaybackDuration]; - if ([playbackDuration unsignedIntegerValue] < [pos unsignedIntegerValue]) { - [response setErrorToInvalidRequestParameterWithMessage:@"pos exceeds the playback duration."]; - return YES; - } - - block = ^{ - _musicPlayer.currentPlaybackTime = pos.doubleValue; - }; - } else if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - if (![weakSelf moviePlayerViewControllerIsPresented]) { - [response setErrorToUnknownWithMessage: - @"Movie player view controller is not presented; " - "please perform Media PUT API first to present the view controller."]; - return YES; - } - NSTimeInterval playbackDuration = [weakSelf viewController].moviePlayer.duration; - if (playbackDuration < [pos unsignedIntegerValue]) { - [response setErrorToInvalidRequestParameterWithMessage:@"pos exceeds the playback duration."]; - return YES; - } - - block = ^{ - _viewController.moviePlayer.currentPlaybackTime = [pos doubleValue]; - }; + NSError *error = nil; + DPHostPlayerBlock block = nil; + if (weakSelf.mediaPlayer) { + block = [weakSelf.mediaPlayer seekPosition:pos error:&error]; } else { - [response setErrorToUnknownWithMessage:@"Unknown player type; this must be a bug."]; + [response setErrorToIllegalServerStateWithMessage:@"Player status is illegal."]; return YES; } - - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - [response setResult:DConnectMessageResultTypeOk]; + [weakSelf runMediaPlayerForBlock:block error:error response:response]; return YES; }]; @@ -736,667 +322,23 @@ - (instancetype)init return self; } -- (void)dealloc -{ - // iTunes関連の通知の削除 - [[NSNotificationCenter defaultCenter] removeObserver:self]; - // 通知終了 - [_musicPlayer endGeneratingPlaybackNotifications]; -} - -// 現在再生中の曲が変わった時の通知 --(void)sendEventMusicWithMessage:(DConnectMessage*)message -{ - - // イベントの取得 - NSArray *evts = [_eventMgr eventListForServiceId:DPHostDevicePluginServiceId - profile:DConnectMediaPlayerProfileName - attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; - - MPMediaItem *mediaItem = _musicPlayer.nowPlayingItem; - if (mediaItem) { - DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithMediaItem:mediaItem]; - if (mediaCtx.mediaId) { - [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:message]; - } - if (mediaCtx.mimeType) { - [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:message]; - } - [DConnectMediaPlayerProfile setPos:_musicPlayer.currentPlaybackTime target:message]; - } - - // イベント送信 - for (DConnectEvent *evt in evts) { - DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; - - [DConnectMediaPlayerProfile setMediaPlayer:message target:eventMsg]; - [SELF_PLUGIN sendEvent:eventMsg]; - } -} - --(void)sendEventMovieWithMessage:(DConnectMessage*)message -{ - - // イベントの取得 - NSArray *evts = [_eventMgr eventListForServiceId:DPHostDevicePluginServiceId - profile:DConnectMediaPlayerProfileName - attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; - - NSURL *contentURL = _viewController.moviePlayer.contentURL; - if (contentURL) { - DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithURL:contentURL]; - if (mediaCtx.mediaId) { - [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:message]; - - } - if (mediaCtx.mimeType) { - [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:message]; - } - - [DConnectMediaPlayerProfile setPos:_viewController.moviePlayer.currentPlaybackTime target:message]; - } - - // イベント送信 - for (DConnectEvent *evt in evts) { - DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; - - [DConnectMediaPlayerProfile setMediaPlayer:message target:eventMsg]; - [SELF_PLUGIN sendEvent:eventMsg]; - } -} - -// 現在再生中の曲が変わった時の通知 --(void) nowPlayingItemChangedInIPod:(NSNotification *)notification -{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - // 再生コンテンツ変更 - NSString *status; - if (_musicPlayer.playbackState == MPMusicPlaybackStateStopped) { - status = DConnectMediaPlayerProfileStatusComplete; - } else { - status = DConnectMediaPlayerProfileStatusComplete; - [_musicPlayer stop]; - - } - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [self sendEventMusicWithMessage:mediaPlayer]; -} - -- (void) nowPlayingItemChangedInMoviePlayer:(NSNotification *)notification -{ - // イベントの取得 - NSArray *evts = [_eventMgr eventListForServiceId:DPHostDevicePluginServiceId - profile:DConnectMediaPlayerProfileName - attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; - - DConnectMessage *mediaPlayer = [DConnectMessage message]; - - // 再生コンテンツ変更 - NSString *status; - NSURL *contentURL = [notification.object contentURL]; - MPMoviePlaybackState playbackState = _viewController.moviePlayer.playbackState; - switch (playbackState) { - case MPMoviePlaybackStateStopped: - status = DConnectMediaPlayerProfileStatusStop; - break; - case MPMoviePlaybackStatePlaying: - status = DConnectMediaPlayerProfileStatusPlay; - break; - case MPMoviePlaybackStatePaused: - status = DConnectMediaPlayerProfileStatusPause; - break; - default: - status = DConnectMediaPlayerProfileStatusMedia; - break; - } - - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - - if (contentURL) { - DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithURL:contentURL]; - if (mediaCtx) { - if (mediaCtx.mediaId) { - [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:mediaPlayer]; - } - if (mediaCtx.mimeType) { - [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:mediaPlayer]; - } - } - - [DConnectMediaPlayerProfile setPos:_musicPlayer.currentPlaybackTime target:mediaPlayer]; - } - - // イベント送信 - for (DConnectEvent *evt in evts) { - DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; - - [DConnectMediaPlayerProfile setMediaPlayer:mediaPlayer target:eventMsg]; - - [SELF_PLUGIN sendEvent:eventMsg]; - } -} - -- (NSArray *)contextsBySearchingAssetsLibraryWithQuery:(NSString *)query - mimeType:(NSString *)mimeType -{ - NSAssert(![NSThread isMainThread], - @"%s can not be invoked from the main queue; please invoke it from the other.", __PRETTY_FUNCTION__); - - __block BOOL failed = NO; - __block NSMutableArray *ctxArr = [NSMutableArray new]; - - @synchronized(_lockAssetsLibraryQuerying) { - // アセットライブラリへのクエリ処理を排他にする。 - - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 30); - - - NSUInteger groupTypes = ALAssetsGroupAll; - NSString *mimeTypeLowercase = mimeType.lowercaseString; - id mainLoopBlock = ^(ALAssetsGroup *group, BOOL *stop1) - { - if (failed) { - // 失敗状態になっているのなら、処理を切り上げる。 - *stop1 = YES; - return; - } - - if(group != nil) { - [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop2) - { - if (failed) { - // 失敗状態になっているのなら、処理を切り上げる。 - *stop2 = YES; - return; - } - - if (result) { - DPHostMediaContext *ctx = [DPHostMediaContext contextWithAsset:result]; - if (!ctx) { - // コンテキスト作成失敗;スキップ - return; - } - - // クエリー検索 - if (query) { - // クエリーのマッチングはファイル名に対して行う。 - NSRange result = [ctx.title rangeOfString:query]; - if (result.location == NSNotFound && result.length == 0) { - // クエリーにマッチせず;スキップ。 - return; - } - } - // MIMEタイプ検索 - if (mimeType) { - NSRange result = [ctx.mimeType rangeOfString:mimeTypeLowercase]; - if (result.location == NSNotFound && result.length == 0) { - // MIMEタイプにマッチせず;スキップ。 - return; - } - } - - @synchronized(ctxArr) { - [ctxArr addObject:ctx]; - } - } - }]; - } else { - // group == nil ⇒ イテレーション終了 - dispatch_semaphore_signal(semaphore); - } - }; - id failBlock = ^(NSError *error) - { - failed = YES; - return; - }; - - [_library enumerateGroupsWithTypes:groupTypes - usingBlock:mainLoopBlock - failureBlock:failBlock]; - - // ライブラリのクエリー(非同期)が終わる、もしくはタイムアウトするまで待つ - long result = dispatch_semaphore_wait(semaphore, timeout); - if (result != 0) { - // タイムアウト - failed = YES; - } - } - - return failed ? nil : ctxArr; -} - -- (NSArray *)contextsBySearchingIPodLibraryWithQuery:(NSString *)query - mimeType:(NSString *)mimeType -{ - NSMutableArray *ctxArr = [NSMutableArray new]; - - @synchronized(_lockIPodLibraryQuerying) { - // iTunes Media - MPMediaQuery *mediaQuery = [MPMediaQuery new]; - [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@(TargetMPMediaType) - forProperty:MPMediaItemPropertyMediaType]]; - [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@(NO) - forProperty:MPMediaItemPropertyIsCloudItem]]; - NSArray *items = [mediaQuery items]; - - NSString *mimeTypeLowsercase = mimeType.lowercaseString; - for (MPMediaItem *mediaItem in items) { - if (!(mediaItem.mediaType == MPMediaTypeMusic | - mediaItem.mediaType == MPMediaTypeHomeVideo)) { - continue; - } - DPHostMediaContext *ctx = [DPHostMediaContext contextWithMediaItem:mediaItem]; - if (!ctx) { - // コンテキスト作成失敗;スキップ - continue; - } - - // クエリー検索 - if (query) { - // クエリーのマッチングはファイル名に対して行う。 - BOOL hit = false; - NSRange result; - // titleとcreators.creatorでマッチングを行う。 - result = [ctx.title rangeOfString:query]; - hit = hit || result.location != NSNotFound || result.length != 0; - - for (int i = 0; i < [ctx.creators count]; ++i) { - result = [[[ctx.creators objectAtIndex:i] - stringForKey:DConnectMediaPlayerProfileParamCreator] - rangeOfString:query]; - hit = hit || result.location != NSNotFound || result.length != 0; - } - - if (!hit) { - // クエリーにマッチせず;スキップ。 - continue; - } - } - // MIMEタイプ検索 - if (mimeType) { - NSRange result = [ctx.mimeType rangeOfString:mimeTypeLowsercase]; - if (result.location == NSNotFound && result.length == 0) { - // MIMEタイプにマッチせず;スキップ。 - continue; - } - } - - @synchronized(ctxArr) { - [ctxArr addObject:ctx]; - } - } - } - - return ctxArr.count == 0 ? nil : ctxArr; -} - -- (MPMoviePlayerViewController *)viewControllerWithURL:(NSURL *)url -{ - MPMoviePlayerViewController *viewController = [[MPMoviePlayerViewController alloc] initWithContentURL:url]; - viewController.moviePlayer.shouldAutoplay = NO; - // 再生完了通知をさせない様にする - // MPMoviePlayerViewControllerの初期動作だと、再生完了時に閉じる。 - // 再生完了時に閉じる処理を実行させたくないので、再生完了通知を一旦消す。 - [[NSNotificationCenter defaultCenter] removeObserver:_viewController - name:MPMoviePlayerPlaybackDidFinishNotification - object:_viewController.moviePlayer]; - // 再生完了の通知;独自の再生完了時に処理を行わせる。 - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(videoFinished:) - name:MPMoviePlayerPlaybackDidFinishNotification - object:viewController.moviePlayer]; - // 再生項目変更の通知 - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(nowPlayingItemChangedInMoviePlayer:) - name:MPMoviePlayerNowPlayingMovieDidChangeNotification - object:viewController.moviePlayer]; - - return viewController; -} - -- (BOOL)moviePlayerViewControllerIsPresented -{ - UIViewController *rootView = [self topViewController]; - [self topViewController:rootView]; - return ([rootView isKindOfClass:[MPMoviePlayerViewController class]]); -} - -- (void) videoFinished:(NSNotification*)notification -{ - - int value = [[notification.userInfo valueForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue]; - if (value == MPMovieFinishReasonUserExited) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_viewController.moviePlayer stop]; - [_viewController dismissMoviePlayerViewControllerAnimated]; - [self nowPlayingItemChangedInMoviePlayer:notification]; - - // 一度閉じたら次回動画再生時には - // 別のMPMoviePlayerViewControllerインスタンスを使うので、 - // オブザーバーを削除しておく。 - [[NSNotificationCenter defaultCenter] removeObserver:self]; - }); - } - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusComplete; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [self sendEventMovieWithMessage:mediaPlayer]; - -} #pragma mark - Private Methods -- (void)checkOrder:(NSString **)sortOrder - sortTarget:(NSString **)sortTarget - response:(DConnectResponseMessage *)response - order:(NSArray *)order -{ - - - if (order) { - if (order.count >= 2) { - *sortTarget = order[0]; - *sortOrder = order[1]; - } - if (!(*sortTarget) || !(*sortOrder)) { - [response setErrorToInvalidRequestParameterWithMessage:@"order is invalid."]; +- (void)runMediaPlayerForBlock:(DPHostPlayerBlock)block + error:(NSError *)error + response:(DConnectResponseMessage *)response { + if (!error) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); } + [response setResult:DConnectMessageResultTypeOk]; } else { - *sortTarget = DConnectMediaPlayerProfileParamTitle; - *sortOrder = DConnectMediaPlayerProfileOrderASC; - } -} - -- (void)compareOrderWithResponse:(DConnectResponseMessage *)response - sortTarget:(NSString *)sortTarget - sortOrder:(NSString *)sortOrder - comparator:(NSComparator *)comparator -{ - // ソート対象のNSStringもしくはNSNumberを返却するブロックを用意する。 - id (^accessor)(id); - NSComparisonResult (^innerComp)(id, id); - if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamMediaId]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj mediaId]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamMIMEType]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj mimeType]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamTitle]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj title]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamType]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj type]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamLanguage]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj language]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamDescription]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj description]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamDuration]) { - accessor = ^id(id obj) { - return [(DPHostMediaContext *)obj duration]; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 compare: obj2]; - }; - } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamImageURI]) { - accessor = ^id(id obj) { - return[(DPHostMediaContext *)obj imageUri].absoluteString; - }; - innerComp = ^NSComparisonResult(id obj1, id obj2) { - return [obj1 localizedCaseInsensitiveCompare: obj2]; - }; - } else { - [response setErrorToInvalidRequestParameterWithMessage:@"order is invalid."]; - } - - - if ([sortOrder isEqualToString:DConnectMediaPlayerProfileOrderASC]) { - *comparator = ^NSComparisonResult(id obj1, id obj2) { - id obj1Tmp = accessor(obj1); - id obj2Tmp = accessor(obj2); - return innerComp(obj1Tmp, obj2Tmp); - }; - } else if ([sortOrder isEqualToString:DConnectMediaPlayerProfileOrderDESC]) { - *comparator = ^NSComparisonResult(id obj1, id obj2) { - id obj1Tmp = accessor(obj1); - id obj2Tmp = accessor(obj2); - return innerComp(obj2Tmp, obj1Tmp); - }; - } else if (![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderASC] - && ![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderDESC]) { - [response setErrorToInvalidRequestParameterWithMessage:@"order is invalid."]; + [response setError:error.code message:error.localizedDescription]; } } -- (void)setIpodMusicMediaWithItem:(MPMediaItem *)mediaItem - response:(DConnectResponseMessage *)response -{ - __weak typeof(self) _self = self; - void(^block)(void) = ^{ - [_musicPlayer setQueueWithQuery:_defaultMediaQuery]; - _musicPlayer.nowPlayingItem = mediaItem; - - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusMedia; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [_self sendEventMusicWithMessage:mediaPlayer]; - - }; - if ([NSThread isMainThread]) { - block(); - } else { - dispatch_sync(dispatch_get_main_queue(), block); - } - - [response setResult:DConnectMessageResultTypeOk]; - - if (_currentMediaPlayer == MediaPlayerTypeMoviePlayer) { - dispatch_async(dispatch_get_main_queue(), ^{ - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusMedia; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [_self sendEventMusicWithMessage:mediaPlayer]; - - - [_viewController.moviePlayer pause]; - [_viewController dismissMoviePlayerViewControllerAnimated]; - }); - } - - _currentMediaPlayer = MediaPlayerTypeIPod; - -} - -- (void)setIpodMovieMediaWithResponse:(DConnectResponseMessage *)response - url:(NSURL *)url - mediaItem:(MPMediaItem *)mediaItem - isIPodMovieMedia:(BOOL)isIPodMovieMedia -{ - NSURL *movieURL = url; - if (isIPodMovieMedia) { - NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; - if ([isCloudItem boolValue]) { - [response setErrorToUnknownWithMessage: - @"Media item specified is an iTunes movie item," - "and it must be downloaded into the iOS device before playing."]; - return; - } - - // iPodライブラリの動画メディアはMoviePlayerを使う。 - // MoviePlayerではメディアのURLが必要なので、MPMediaItemからAssetURLを取得する。 - movieURL = [mediaItem valueForProperty:MPMediaItemPropertyAssetURL]; - if (!movieURL) { - [response setErrorToUnknownWithMessage: - @"Failed to pass the specified media item to the movie player;" - "perhaps this media item is a protected media only playable " - "in the official apps like \"Music\" and \"Videos\"."]; - return; - } - } - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; - [asset loadValuesAsynchronouslyForKeys:@[@"hasProtectedContent", @"playable"] completionHandler: - ^{ - NSError *error = nil; - - AVKeyValueStatus keyStatus; - keyStatus = [asset statusOfValueForKey:@"playable" error:&error]; - if (keyStatus == AVKeyValueStatusFailed || error) - { - [response setErrorToUnknownWithMessage: - @"Operation aborted; Failed to determine " - "whether the specified media item is protected or not."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - - if (!asset.playable) { - // 再生できない - [response setErrorToUnknownWithMessage:@"Media item is not playable."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - - keyStatus = [asset statusOfValueForKey:@"hasProtectedContent" error:&error]; - if (keyStatus == AVKeyValueStatusFailed || error) - { - [response setErrorToUnknownWithMessage: - @"Operation aborted; Failed to determine whether the specified media item is protected or not."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - - if (asset.hasProtectedContent) { - // 保護コンテンツを持っている;再生できない - [response setErrorToUnknownWithMessage: - @"Media item specified is an iTunes movie item and is protected;" - " protected movie media items are playable only " - "in the official player apps like Music and Videos."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - - if (_currentMediaPlayer == MediaPlayerTypeIPod && - _musicPlayer.playbackState == MPMusicPlaybackStatePlaying) { - // iPodミュージックプレイヤーが再生されている場合は、再生を一時停止する。 - dispatch_async(dispatch_get_main_queue(), ^{ - [_musicPlayer pause]; - }); - } - __weak typeof(self) _self = self; - void(^block)(void) = ^{ - _viewController.moviePlayer.movieSourceType = MPMovieSourceTypeFile; - // 再生項目変更は、2度目以降ではprepareToPlayする - [_viewController.moviePlayer prepareToPlay]; - [response setResult:DConnectMessageResultTypeOk]; - _currentMediaPlayer = MediaPlayerTypeMoviePlayer; - DConnectMessage *mediaPlayer = [DConnectMessage message]; - NSString *status = DConnectMediaPlayerProfileStatusMedia; - [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; - [_self sendEventMovieWithMessage:mediaPlayer]; - [[DConnectManager sharedManager] sendResponse:response]; - }; - if ([self moviePlayerViewControllerIsPresented]) { - block(); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - _self.viewController = [_self viewControllerWithURL:movieURL]; - block(); - [[_self topViewController] presentMoviePlayerViewControllerAnimated:_viewController]; - }); - } - }]; - -} - -- (BOOL) putMediaRequest:(DConnectRequestMessage *) request - response:(DConnectResponseMessage *)response - serviceId:(NSString *)serviceId - mediaId:(NSString *)mediaId -{ - NSURL *url = [NSURL URLWithString:mediaId]; - DPHostMediaContext *ctx = [DPHostMediaContext contextWithURL:url]; - if (!ctx) { - [response setErrorToInvalidRequestParameterWithMessage:@"MediaId is Invalid."]; - return YES; - } - _nowPlayingMediaId = mediaId; - NSNumber *persistentId; - MPMediaItem *mediaItem; - BOOL isIPodAudioMedia = [url.scheme isEqualToString:MediaContextMediaIdSchemeIPodAudio]; - BOOL isIPodMovieMedia = [url.scheme isEqualToString:MediaContextMediaIdSchemeIPodMovie]; - if (isIPodAudioMedia || isIPodMovieMedia) { - persistentId = [DPHostMediaContext persistentIdWithMediaIdURL:url]; - - MPMediaQuery *mediaQuery = [self defaultMediaQuery].copy; - [mediaQuery addFilterPredicate: - [MPMediaPropertyPredicate predicateWithValue:persistentId - forProperty:MPMediaItemPropertyPersistentID]]; - NSArray *items = [mediaQuery items]; - - if (items.count == 0) { - [response setErrorToInvalidRequestParameterWithMessage:@"Media specified by mediaId does not found."]; - return YES; - } - mediaItem = items[0]; - } - - if (isIPodAudioMedia) { - [self setIpodMusicMediaWithItem:mediaItem response:response]; - return YES; - } - [self setIpodMovieMediaWithResponse:response url:url mediaItem:mediaItem isIPodMovieMedia:isIPodMovieMedia]; - return NO; -} - - - -#pragma mark - Get topMost viewController -- (UIViewController *)topViewController{ - return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController]; -} - -- (UIViewController *)topViewController:(UIViewController *)rootViewController -{ - if (rootViewController.presentedViewController == nil) { - return rootViewController; - } - - if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) { - UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController; - UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; - return [self topViewController:lastViewController]; - } - - UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController; - return [self topViewController:presentedViewController]; -} @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.h new file mode 100644 index 00000000..13c0ea12 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.h @@ -0,0 +1,14 @@ +// +// DPHostIPodAudioPlayer.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + +#import "DPHostMediaPlayer.h" + +@interface DPHostIPodAudioPlayer : DPHostMediaPlayer + +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m new file mode 100644 index 00000000..243b7765 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m @@ -0,0 +1,288 @@ +// +// DPHostIPodAudioPlayer.m +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + +#import "DPHostIPodAudioPlayer.h" +#import "DPHostUtils.h" +@interface DPHostIPodAudioPlayer() +// iPodプレイヤー +@property MPMusicPlayerController *musicPlayer; + +// iPodプレイヤークエリー +@property MPMediaQuery *defaultMediaQuery; + +// 再生中のMediaItem +@property MPMediaItem *currentItem; + +@end + +@implementation DPHostIPodAudioPlayer + +- (instancetype)initWithMediaContext:(DPHostMediaContext *)ctx plugin:(DPHostDevicePlugin *)plugin error:(NSError **)error +{ + self = [super initWithMediaContext:ctx plugin:plugin error:error]; + if (self) { + // iPodプレイヤーを取得 + self.musicPlayer = [MPMusicPlayerController iPodMusicPlayer]; + self.musicPlayer.shuffleMode = MPMusicShuffleModeOff; + self.musicPlayer.repeatMode = MPMusicRepeatModeOne; + self.defaultMediaQuery = [MPMediaQuery songsQuery]; + [self.defaultMediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger:TargetMPMediaType] + forProperty:MPMediaItemPropertyMediaType]]; + NSNumber *persistentId = [DPHostMediaContext persistentIdWithMediaIdURL:[NSURL URLWithString:ctx.mediaId]]; + MPMediaQuery *mediaQuery = self.defaultMediaQuery.copy; + [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; + NSArray *items = [mediaQuery items]; + if (items.count == 0) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found"]; + return nil; + } + self.currentItem = items[0]; + self.defaultMediaQuery = mediaQuery; + [self setIpodMusicMediaWithItem:self.currentItem]; + } + return self; +} + +- (NSString*)playStatus +{ + NSString *status; + // iPodミュージックプレイヤー + switch (self.musicPlayer.playbackState) { + case MPMusicPlaybackStateStopped: + status = DConnectMediaPlayerProfileStatusStop; + break; + case MPMusicPlaybackStatePlaying: + status = DConnectMediaPlayerProfileStatusPlay; + break; + case MPMusicPlaybackStatePaused: + status = DConnectMediaPlayerProfileStatusPause; + break; + default: + status = DConnectMediaPlayerProfileStatusStop; + } + return status; +} + +- (DPHostPlayerBlock)playWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + MPMediaItem *mediaItem = self.musicPlayer.nowPlayingItem; + if (self.currentItem && self.musicPlayer.playbackState == MPMusicPlaybackStateStopped) { + [self setIpodMusicMediaWithItem:self.currentItem]; + } + NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; + if (isCloudItem) { + DPHostReachability *networkReachability + = [DPHostReachability reachabilityForInternetConnection]; + NetworkStatus networkStatus = [networkReachability currentReachabilityStatus]; + if ([isCloudItem boolValue] && networkStatus == NotReachable) { + // iCloud上の音楽項目 + // (つまりiOSデバイス側にまだダウンロードされていない)で、 + // 尚かつインターネット接続が無い場合は + // 再生できない。 + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Internet is not reachable; the specified audio media item " + "is an iClould item and its playback requires an Internet connection."]; + return block; + } + } + __weak DPHostIPodAudioPlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusPlay; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMusicWithMessage:mediaPlayer]; + [[weakSelf musicPlayer] setCurrentPlaybackTime:0.0f]; + [[weakSelf musicPlayer] play]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + // 現在再生中の曲が変わった時の通知 + [notificationCenter addObserver:weakSelf + selector:@selector(nowPlayingItemChangedInIPod:) + name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification + object:weakSelf.musicPlayer]; + + // 通知開始 + [weakSelf.musicPlayer beginGeneratingPlaybackNotifications]; + }; + return block; +} + + +- (DPHostPlayerBlock)stopWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (self.musicPlayer.playbackState == MPMusicPlaybackStateStopped) { + return block; + } + __weak DPHostIPodAudioPlayer *weakSelf = self; + block = ^{ + // iTunes関連の通知の削除 + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter removeObserver:weakSelf + name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification + object:weakSelf.musicPlayer]; + // 通知終了 + [weakSelf.musicPlayer endGeneratingPlaybackNotifications]; + + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusStop; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMusicWithMessage:mediaPlayer]; + [weakSelf.musicPlayer stop]; + }; + return block; +} + +- (DPHostPlayerBlock)pauseWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (self.musicPlayer.playbackState == MPMusicPlaybackStatePlaying) { + __weak DPHostIPodAudioPlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusPause; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMusicWithMessage:mediaPlayer]; + [weakSelf.musicPlayer pause]; + }; + } else { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; + } + return block; +} + +- (DPHostPlayerBlock)resumeWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (self.musicPlayer.playbackState == MPMusicPlaybackStatePaused) { + MPMediaItem *mediaItem = self.musicPlayer.nowPlayingItem; + NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; + if (isCloudItem) { + DPHostReachability *networkReachability + = [DPHostReachability reachabilityForInternetConnection]; + NetworkStatus networkStatus = [networkReachability currentReachabilityStatus]; + if ([isCloudItem boolValue] && networkStatus == NotReachable) { + // iCloud上の音楽項目 + //(つまりiOSデバイス側にまだダウンロードされていない)で、 + //尚かつインターネット接続が無い場合は + // 再生できない。 + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Internet is not reachable;" + " the specified audio media item is an iClould " + "item and its playback requires an Internet connection."]; + return block; + } + } + __weak DPHostIPodAudioPlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusResume; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMusicWithMessage:mediaPlayer]; + [weakSelf.musicPlayer play]; + }; + } else { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; + } + return block; +} + +- (NSTimeInterval)seekStatusWithError:(NSError **)error +{ + return self.musicPlayer.currentPlaybackTime; +} + +- (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + MPMediaItem *nowPlayingItem = self.musicPlayer.nowPlayingItem; + NSNumber *playbackDuration = [nowPlayingItem valueForProperty:MPMediaItemPropertyPlaybackDuration]; + if ([playbackDuration unsignedIntegerValue] < [position unsignedIntegerValue]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"pos exceeds the playback duration."]; + return block; + } + __weak DPHostIPodAudioPlayer *weakSelf = self; + block = ^{ + weakSelf.musicPlayer.currentPlaybackTime = position.doubleValue; + }; + return block; +} + +#pragma mark - Private Method +- (void)setIpodMusicMediaWithItem:(MPMediaItem *)mediaItem +{ + __weak typeof(self) weakSelf = self; + void(^block)(void) = ^{ + [weakSelf.musicPlayer setQueueWithQuery:weakSelf.defaultMediaQuery]; + weakSelf.musicPlayer.nowPlayingItem = mediaItem; + + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusMedia; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf.self sendEventMusicWithMessage:mediaPlayer]; + + }; + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +// 現在再生中の曲が変わった時の通知 +-(void)sendEventMusicWithMessage:(DConnectMessage*)message +{ + + // イベントの取得 + NSArray *evts = [super.eventMgr eventListForServiceId:DPHostDevicePluginServiceId + profile:DConnectMediaPlayerProfileName + attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; + + MPMediaItem *mediaItem = self.musicPlayer.nowPlayingItem; + if (mediaItem) { + DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithMediaItem:mediaItem]; + if (mediaCtx.mediaId) { + [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:message]; + } + if (mediaCtx.mimeType) { + [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:message]; + } + [DConnectMediaPlayerProfile setPos:self.musicPlayer.currentPlaybackTime target:message]; + } + + // イベント送信 + for (DConnectEvent *evt in evts) { + DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; + + [DConnectMediaPlayerProfile setMediaPlayer:message target:eventMsg]; + [SUPER_PLUGIN sendEvent:eventMsg]; + } +} + +#pragma mark - Ipod Music Notification + +// 現在再生中の曲が変わった時の通知 +-(void) nowPlayingItemChangedInIPod:(NSNotification *)notification +{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + // 再生コンテンツ変更 + NSString *status; + if (self.musicPlayer.playbackState == MPMusicPlaybackStateStopped) { + status = DConnectMediaPlayerProfileStatusComplete; + } else { + status = DConnectMediaPlayerProfileStatusComplete; + [self.musicPlayer stop]; + + } + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [self sendEventMusicWithMessage:mediaPlayer]; +} +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h new file mode 100644 index 00000000..810b56c7 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h @@ -0,0 +1,70 @@ +// +// DPHostMediaPlayer.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + + +#import +#import +#import +#import +#import + +#import + +#import "DPHostDevicePlugin.h" +#import "DPHostMediaPlayerProfile.h" +#import "DPHostMediaContext.h" +#import "DPHostReachability.h" +#import "DPHostService.h" +#import "DPHostUtil.h" + +@interface DPHostMediaPlayer : NSObject +// @brief メディア処理の実行用Block +typedef void (^DPHostPlayerBlock)(void); + +// @brief イベントマネージャ +@property DConnectEventManager *eventMgr; + +// @brief DevicePlugin +@property DPHostDevicePlugin *plugin; + +// PUT /mediaPlayer/media +- (instancetype)initWithMediaContext:(DPHostMediaContext*)ctx + plugin:(DPHostDevicePlugin*)plugin + error:(NSError**)error; + +// GET /mediaPlayer/status +- (NSString*)playStatus; + +// PUT /mediaPlayer/play +- (DPHostPlayerBlock)playWithError:(NSError**)error; + +// PUT /mediaPlayer/stop +- (DPHostPlayerBlock)stopWithError:(NSError**)error; + +// PUT /mediaPlayer/pause +- (DPHostPlayerBlock)pauseWithError:(NSError**)error; + +// PUT /mediaPlayer/resume +- (DPHostPlayerBlock)resumeWithError:(NSError**)error; + +// GET /mediaPlayer/seek +- (NSTimeInterval)seekStatusWithError:(NSError**)error; + +// PUT /mediaPlayer/seek +- (DPHostPlayerBlock)seekPosition:(NSNumber*)position error:(NSError**)error; + +//NSErrorを生成 ++ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString*)message; + +// TOPにあるViewControllerを返す ++ (UIViewController*)topViewController; ++ (UIViewController*)topViewController:(UIViewController*)rootViewController; + + +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m new file mode 100644 index 00000000..56f2db5c --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m @@ -0,0 +1,99 @@ +// +// DPHostMediaPlayer.m +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + + +#import "DPHostMediaPlayer.h" + +// Error Domain +static NSString *const kDPHostMediaPlayerErrorDomain = @"org.deviceconnect.ios.deviceplugin.host.mediaplayer.error"; + +@implementation DPHostMediaPlayer + +- (instancetype)initWithMediaContext:(DPHostMediaContext *)ctx + plugin:(DPHostDevicePlugin *)plugin + error:(NSError **)error +{ + self = [super init]; + if (self) { + self.eventMgr = [DConnectEventManager sharedManagerForClass:[DPHostDevicePlugin class]]; + self.plugin = plugin; + } + return self; +} + +- (NSString*)playStatus +{ + // override subclass + return nil; +} + +- (DPHostPlayerBlock)playWithError:(NSError **)error +{ + // override subclass + return nil; +} + +- (DPHostPlayerBlock)stopWithError:(NSError **)error +{ + // override subclass + return nil; +} + +- (DPHostPlayerBlock)pauseWithError:(NSError **)error +{ + // override subclass + return nil; +} + +- (DPHostPlayerBlock)resumeWithError:(NSError **)error +{ + // override subclass + return nil; +} + +- (NSTimeInterval)seekStatusWithError:(NSError **)error +{ + // override subclass + return 0.0f; +} + +- (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error +{ + //override subclass + return nil; +} + +#pragma mark - Utils method ++ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString *)message +{ + NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; + errorDetail[NSLocalizedDescriptionKey] = message; + return [NSError errorWithDomain:kDPHostMediaPlayerErrorDomain code:code userInfo:errorDetail.mutableCopy].copy; +} + ++ (UIViewController*)topViewController +{ + return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController]; +} + ++ (UIViewController*)topViewController:(UIViewController *)rootViewController +{ + if (!rootViewController.presentedViewController) { + return rootViewController; + } + if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController*) rootViewController.presentedViewController; + UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; + return [self topViewController:lastViewController]; + } + + UIViewController *presentedViewController = (UIViewController*) rootViewController.presentedViewController; + return [self topViewController:presentedViewController]; +} +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.h new file mode 100644 index 00000000..9b385210 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.h @@ -0,0 +1,26 @@ +// +// DPHostMediaPlayerFactory.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + + +#import +#import +#import "DPHostMediaPlayer.h" +#import "DPHostMediaContext.h" +@interface DPHostMediaPlayerFactory : NSObject ++ (DPHostMediaPlayer*)createPlayerWithMediaId:(NSString*)mediaId + plugin:(DPHostDevicePlugin*)plugin + error:(NSError**)error; + ++ (NSArray*)searchMediaWithQuery:(NSString*)query + mimeType:(NSString*)mimeType + order:(NSString*)order + offset:(NSNumber*)offset + limit:(NSNumber*)limit + error:(NSError**)error; +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m new file mode 100644 index 00000000..dba45abc --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m @@ -0,0 +1,364 @@ +// +// DPHostMediaPlayerFactory.m +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// +#import +#import +#import +#import + +#import "DPHostMediaPlayerFactory.h" +#import "DPHostIPodAudioPlayer.h" +#import "DPHostMoviePlayer.h" +#import "DPHostUtils.h" + +@implementation DPHostMediaPlayerFactory ++ (DPHostMediaPlayer*)createPlayerWithMediaId:(NSString *)mediaId plugin:(DPHostDevicePlugin *)plugin error:(NSError **)error +{ + NSURL *url = [NSURL URLWithString:mediaId]; + DPHostMediaPlayer *player = nil; + DPHostMediaContext *ctx = [DPHostMediaContext contextWithURL:url]; + if (!ctx) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"MediaId is Invalid"]; + return nil; + } + BOOL isIPodAudioMedia = [url.scheme isEqualToString:MediaContextMediaIdSchemeIPodAudio]; + if (isIPodAudioMedia) { + player = [[DPHostIPodAudioPlayer alloc] initWithMediaContext:ctx plugin:plugin error:error]; + } else { + player = [[DPHostMoviePlayer alloc] initWithMediaContext:ctx plugin:plugin error:error]; + } + return player; +} + ++ (NSArray*)searchMediaWithQuery:(NSString *)query + mimeType:(NSString *)mimeType + order:(NSString *)order + offset:(NSNumber *)offset + limit:(NSNumber *)limit + error:(NSError**)error +{ + NSString *sortTarget; + NSString *sortOrder; + NSArray *orders = nil; + + if (order) { + orders = [order componentsSeparatedByString:@","]; + } + [self checkOrder:&sortOrder + sortTarget:&sortTarget + order:orders + error:error]; + if (*error) { + return nil; + } + NSComparator comp; + [self compareOrderWithSortTarget:sortTarget sortOrder:sortOrder comparator:&comp error:error]; + if (*error) { + return nil; + } + + if (offset && offset.integerValue < 0) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"offset must be a non-negative value."]; + return nil; + } + if (limit && limit.integerValue < 0) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"limit must be a positive value."]; + return nil; + } + NSString *limitString = [limit stringValue]; + NSString *offsetString = [offset stringValue]; + if (![DPHostUtils existDigitWithString:limitString]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + message:@"limit must be a digit number."]; + return nil; + } + if (![DPHostUtils existDigitWithString:offsetString]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + message:@"offset must be a digit number."]; + return nil; + } + NSMutableArray *ctxArr = [NSMutableArray array]; + [ctxArr addObjectsFromArray:[self contextsBySearchingAssetsLibraryWithQuery:query mimeType:mimeType]]; + [ctxArr addObjectsFromArray:[self contextsBySearchingIPodLibraryWithQuery:query mimeType:mimeType]]; + if (offset && offset.integerValue >= ctxArr.count) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + message:@"offset exceeds the size of the media list."]; + return nil; + } + + // 並び替えを実行 + NSArray *tmpArr = [ctxArr sortedArrayUsingComparator:comp]; + + // ページングのために配列の一部分だけ抜き出し + if (offset || limit) { + NSUInteger offsetVal = offset ? offset.unsignedIntegerValue : 0; + NSUInteger limitVal = limit ? limit.unsignedIntegerValue : ctxArr.count; + tmpArr = [tmpArr subarrayWithRange: + NSMakeRange(offset.unsignedIntegerValue, + MIN(ctxArr.count - offsetVal, limitVal))]; + } + return tmpArr.mutableCopy; +} + +#pragma mark - Private Method ++ (NSArray *)contextsBySearchingAssetsLibraryWithQuery:(NSString *)query + mimeType:(NSString *)mimeType +{ + __block BOOL failed = NO; + __block NSMutableArray *ctxArr = [NSMutableArray new]; + + ALAssetsLibrary *library = [ALAssetsLibrary new]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 30); + NSUInteger groupTypes = ALAssetsGroupAll; + NSString *mimeTypeLowercase = mimeType.lowercaseString; + id mainLoopBlock = ^(ALAssetsGroup *group, BOOL *stop1) + { + if (failed) { + // 失敗状態になっているのなら、処理を切り上げる。 + *stop1 = YES; + return; + } + + if(group != nil) { + [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop2) + { + if (failed) { + // 失敗状態になっているのなら、処理を切り上げる。 + *stop2 = YES; + return; + } + + if (result) { + DPHostMediaContext *ctx = [DPHostMediaContext contextWithAsset:result]; + if (!ctx) { + // コンテキスト作成失敗;スキップ + return; + } + + // クエリー検索 + if (query) { + // クエリーのマッチングはファイル名に対して行う。 + NSRange result = [ctx.title rangeOfString:query]; + if (result.location == NSNotFound && result.length == 0) { + // クエリーにマッチせず;スキップ。 + return; + } + } + // MIMEタイプ検索 + if (mimeType) { + NSRange result = [ctx.mimeType rangeOfString:mimeTypeLowercase]; + if (result.location == NSNotFound && result.length == 0) { + // MIMEタイプにマッチせず;スキップ。 + return; + } + } + + @synchronized(ctxArr) { + [ctxArr addObject:ctx]; + } + } + }]; + } else { + // group == nil ⇒ イテレーション終了 + dispatch_semaphore_signal(semaphore); + } + }; + id failBlock = ^(NSError *error) + { + failed = YES; + return; + }; + + [library enumerateGroupsWithTypes:groupTypes + usingBlock:mainLoopBlock + failureBlock:failBlock]; + + // ライブラリのクエリー(非同期)が終わる、もしくはタイムアウトするまで待つ + long result = dispatch_semaphore_wait(semaphore, timeout); + if (result != 0) { + // タイムアウト + failed = YES; + } + + return failed ? nil : ctxArr; +} + ++ (NSArray *)contextsBySearchingIPodLibraryWithQuery:(NSString *)query + mimeType:(NSString *)mimeType +{ + NSMutableArray *ctxArr = [NSMutableArray new]; + + // iTunes Media + MPMediaQuery *mediaQuery = [MPMediaQuery new]; + [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@(TargetMPMediaType) + forProperty:MPMediaItemPropertyMediaType]]; + [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:@(NO) + forProperty:MPMediaItemPropertyIsCloudItem]]; + NSArray *items = [mediaQuery items]; + NSString *mimeTypeLowsercase = mimeType.lowercaseString; + for (MPMediaItem *mediaItem in items) { + if (!(mediaItem.mediaType == MPMediaTypeMusic | + mediaItem.mediaType == MPMediaTypeHomeVideo)) { + continue; + } + DPHostMediaContext *ctx = [DPHostMediaContext contextWithMediaItem:mediaItem]; + if (!ctx) { + // コンテキスト作成失敗;スキップ + continue; + } + + // クエリー検索 + if (query) { + // クエリーのマッチングはファイル名に対して行う。 + BOOL hit = false; + NSRange result; + // titleとcreators.creatorでマッチングを行う。 + result = [ctx.title rangeOfString:query]; + hit = hit || result.location != NSNotFound || result.length != 0; + + for (int i = 0; i < [ctx.creators count]; ++i) { + result = [[[ctx.creators objectAtIndex:i] + stringForKey:DConnectMediaPlayerProfileParamCreator] + rangeOfString:query]; + hit = hit || result.location != NSNotFound || result.length != 0; + } + + if (!hit) { + // クエリーにマッチせず;スキップ。 + continue; + } + } + // MIMEタイプ検索 + if (mimeType) { + NSRange result = [ctx.mimeType rangeOfString:mimeTypeLowsercase]; + if (result.location == NSNotFound && result.length == 0) { + // MIMEタイプにマッチせず;スキップ。 + continue; + } + } + + @synchronized(ctxArr) { + [ctxArr addObject:ctx]; + } + } + + return ctxArr.count == 0 ? nil : ctxArr; +} + + + ++ (void)checkOrder:(NSString **)sortOrder + sortTarget:(NSString **)sortTarget + order:(NSArray *)order + error:(NSError**)error +{ + + if (order) { + if (order.count >= 2) { + *sortTarget = order[0]; + *sortOrder = order[1]; + } + if (!(*sortTarget) || !(*sortOrder)) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + } + } else { + *sortTarget = DConnectMediaPlayerProfileParamTitle; + *sortOrder = DConnectMediaPlayerProfileOrderASC; + } +} + ++ (void)compareOrderWithSortTarget:(NSString *)sortTarget + sortOrder:(NSString *)sortOrder + comparator:(NSComparator *)comparator + error:(NSError**)error +{ + // ソート対象のNSStringもしくはNSNumberを返却するブロックを用意する。 + id (^accessor)(id); + NSComparisonResult (^innerComp)(id, id); + if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamMediaId]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj mediaId]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamMIMEType]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj mimeType]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamTitle]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj title]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamType]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj type]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamLanguage]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj language]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamDescription]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj description]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamDuration]) { + accessor = ^id(id obj) { + return [(DPHostMediaContext *)obj duration]; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare: obj2]; + }; + } else if ([sortTarget isEqualToString:DConnectMediaPlayerProfileParamImageURI]) { + accessor = ^id(id obj) { + return[(DPHostMediaContext *)obj imageUri].absoluteString; + }; + innerComp = ^NSComparisonResult(id obj1, id obj2) { + return [obj1 localizedCaseInsensitiveCompare: obj2]; + }; + } else { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + } + + + if ([sortOrder isEqualToString:DConnectMediaPlayerProfileOrderASC]) { + *comparator = ^NSComparisonResult(id obj1, id obj2) { + id obj1Tmp = accessor(obj1); + id obj2Tmp = accessor(obj2); + return innerComp(obj1Tmp, obj2Tmp); + }; + } else if ([sortOrder isEqualToString:DConnectMediaPlayerProfileOrderDESC]) { + + *comparator = ^NSComparisonResult(id obj1, id obj2) { + id obj1Tmp = accessor(obj1); + id obj2Tmp = accessor(obj2); + return innerComp(obj2Tmp, obj1Tmp); + }; + } else if (![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderASC] + && ![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderDESC]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + } +} + +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.h new file mode 100644 index 00000000..a57d0dbf --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.h @@ -0,0 +1,15 @@ +// +// DPHostMoviePlayer.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + + +#import "DPHostMediaPlayer.h" + +@interface DPHostMoviePlayer : DPHostMediaPlayer +- (void)closeMoviePlayerViewController; +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m new file mode 100644 index 00000000..cf40f36d --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m @@ -0,0 +1,433 @@ +// +// DPHostMoviePlayer.m +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + + +#import "DPHostMoviePlayer.h" +#import "DPHostUtils.h" +@interface DPHostMoviePlayer() +// 動画再生用ビューコントローラー +@property MPMoviePlayerViewController *viewController; +// iPodプレイヤーのクエリー +@property MPMediaQuery *defaultMediaQuery; + +// Currentの動画URL +@property NSURL *currentContentURL; +@end + +@implementation DPHostMoviePlayer +- (instancetype)initWithMediaContext:(DPHostMediaContext *)ctx plugin:(DPHostDevicePlugin *)plugin error:(NSError **)error +{ + self = [super initWithMediaContext:ctx plugin:plugin error:error]; + if (self) { + self.defaultMediaQuery = [MPMediaQuery songsQuery]; + [self.defaultMediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger:TargetMPMediaType] + forProperty:MPMediaItemPropertyMediaType]]; + NSURL *url = [NSURL URLWithString:ctx.mediaId]; + MPMediaItem *mediaItem; + + BOOL isIPodMovieMedia = [url.scheme isEqualToString:MediaContextMediaIdSchemeIPodMovie]; + NSNumber *persistentId = [DPHostMediaContext persistentIdWithMediaIdURL:url]; + if (isIPodMovieMedia) { + MPMediaQuery *mediaQuery = self.defaultMediaQuery.copy; + + [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; + NSArray *items = [mediaQuery items]; + if (items.count == 0) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found."]; + return nil; + } + mediaItem = items[0]; + } + self.currentContentURL = url; + [self setIpodMovieMediaWithUrl:url mediaItem:mediaItem isIPodMovieMedia:isIPodMovieMedia error:error]; + } + return self; +} + +- (NSString*)playStatus +{ + NSString *status; + // MoviePlayer + switch (self.viewController.moviePlayer.playbackState) { + case MPMoviePlaybackStateStopped: + status = DConnectMediaPlayerProfileStatusStop; + break; + case MPMoviePlaybackStatePlaying: + status = DConnectMediaPlayerProfileStatusPlay; + break; + case MPMoviePlaybackStatePaused: + status = DConnectMediaPlayerProfileStatusPause; + break; + default: + status = DConnectMediaPlayerProfileStatusStop; + } + return status; +} + +- (DPHostPlayerBlock)playWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (![self moviePlayerViewControllerIsPresented] && self.currentContentURL) { + NSURL *contentURL = self.currentContentURL; + BOOL isIPodMovieMedia = [contentURL.scheme isEqualToString:MediaContextMediaIdSchemeIPodMovie]; + MPMediaItem *mediaItem; + if (isIPodMovieMedia) { + MPMediaQuery *mediaQuery = [self defaultMediaQuery].copy; + NSNumber *persistentId = [DPHostMediaContext persistentIdWithMediaIdURL:contentURL]; + + [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; + NSArray *items = [mediaQuery items]; + if (items.count == 0) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + message:@"Media specified by mediaId does not found."]; + return block; + } + mediaItem = items[0]; + } + [self setIpodMovieMediaWithUrl:contentURL mediaItem:mediaItem isIPodMovieMedia:isIPodMovieMedia error:error]; + return block; + } + if (self.viewController.moviePlayer.playbackState != MPMoviePlaybackStatePlaying) { + if (self.viewController.moviePlayer.contentURL) { + __weak DPHostMoviePlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusPlay; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMovieWithMessage:mediaPlayer]; + [weakSelf.viewController.moviePlayer setCurrentPlaybackRate:0.0f]; + [weakSelf.viewController.moviePlayer play]; + + }; + return block; + } + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be played; media is not specified."]; + } + return block; +} + +- (DPHostPlayerBlock)stopWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (self.viewController.moviePlayer.playbackState == MPMoviePlaybackStateStopped) { + return block; + } + if (![self moviePlayerViewControllerIsPresented]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Movie player view controller is not presented;" + "please perform Media PUT API first to present the view controller."]; + } else { + __weak DPHostMoviePlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusStop; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMovieWithMessage:mediaPlayer]; + // ムービープレイヤーを閉じる。 + [weakSelf.viewController.moviePlayer stop]; + [weakSelf.viewController dismissMoviePlayerViewControllerAnimated]; + weakSelf.viewController = nil; + }; + } + return block; +} + +- (DPHostPlayerBlock)pauseWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (![self moviePlayerViewControllerIsPresented]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Movie player view controller is not presented;" + " please perform Media PUT API first to present the view controller."]; + } else { + if (self.viewController.moviePlayer.playbackState == MPMusicPlaybackStatePlaying) { + __weak DPHostMoviePlayer *weakSelf =self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusPause; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMovieWithMessage:mediaPlayer]; + [weakSelf.viewController.moviePlayer pause]; + }; + } else { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; + } + } + return block; +} + +- (DPHostPlayerBlock)resumeWithError:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (![self moviePlayerViewControllerIsPresented]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Movie player view controller is not presented;" + "please perform Media PUT API first to present the view controller."]; + } else { + if (self.viewController.moviePlayer.playbackState == MPMoviePlaybackStatePaused) { + __weak DPHostMoviePlayer *weakSelf = self; + block = ^{ + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusResume; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMovieWithMessage:mediaPlayer]; + [weakSelf.viewController.moviePlayer play]; + }; + } else { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; + } + } + return block; +} + +- (NSTimeInterval)seekStatusWithError:(NSError **)error +{ + if (!self.viewController) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Movie player view controller is not presented;" + "please perform Media PUT API first to present the view controller."]; + return -1.0f; + } + return self.viewController.moviePlayer.playableDuration; +} + +- (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error +{ + DPHostPlayerBlock block = ^{}; + if (![self moviePlayerViewControllerIsPresented]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Movie player view controller is not presented; " + "please perform Media PUT API first to present the view controller."]; + return block; + } + NSTimeInterval playbackDuration = self.viewController.moviePlayer.duration; + if (playbackDuration < [position unsignedIntegerValue]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"pos exceeds the playback duration."]; + return block; + } + __weak DPHostMoviePlayer *weakSelf = self; + block = ^{ + weakSelf.viewController.moviePlayer.currentPlaybackTime = [position doubleValue]; + }; + return block; +} + +#pragma mark - ETC public method +- (void)closeMoviePlayerViewController +{ + [self.viewController dismissMoviePlayerViewControllerAnimated]; + [self.viewController dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - Private Method +- (void)setIpodMovieMediaWithUrl:(NSURL *)url + mediaItem:(MPMediaItem *)mediaItem + isIPodMovieMedia:(BOOL)isIPodMovieMedia + error:(NSError**)error +{ + NSURL *movieURL = url; + if (isIPodMovieMedia) { + NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; + if ([isCloudItem boolValue]) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Media item specified is an iTunes movie item," + "and it must be downloaded into the iOS device before playing."]; + return; + } + + // iPodライブラリの動画メディアはMoviePlayerを使う。 + // MoviePlayerではメディアのURLが必要なので、MPMediaItemからAssetURLを取得する。 + movieURL = [mediaItem valueForProperty:MPMediaItemPropertyAssetURL]; + if (!movieURL) { + *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Failed to pass the specified media item to the movie player;" + "perhaps this media item is a protected media only playable " + "in the official apps like \"Music\" and \"Videos\"."]; + return; + } + } + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5); + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:movieURL options:nil]; + __weak DPHostMoviePlayer *weakSelf = self; + [asset loadValuesAsynchronouslyForKeys:@[@"hasProtectedContent", @"playable"] completionHandler: + ^{ + void(^block)(void) = ^{ + weakSelf.viewController.moviePlayer.movieSourceType = MPMovieSourceTypeFile; + // 再生項目変更は、2度目以降ではprepareToPlayする + [weakSelf.viewController.moviePlayer prepareToPlay]; + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusMedia; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [weakSelf sendEventMovieWithMessage:mediaPlayer]; + + }; + if ([weakSelf moviePlayerViewControllerIsPresented]) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.viewController = [weakSelf viewControllerWithURL:movieURL]; + self.viewController.moviePlayer.shouldAutoplay = YES; + block(); + [[DPHostMediaPlayer topViewController] presentMoviePlayerViewControllerAnimated:weakSelf.viewController]; + }); + } + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, timeout); +} +- (BOOL)moviePlayerViewControllerIsPresented +{ + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3); + __block UIViewController *rootView = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + rootView = [DPHostMediaPlayer topViewController]; + [DPHostMediaPlayer topViewController:rootView]; + dispatch_semaphore_signal(semaphore); + }); + dispatch_semaphore_wait(semaphore, timeout); + return (rootView && [rootView isKindOfClass:[MPMoviePlayerViewController class]]); +} +-(void)sendEventMovieWithMessage:(DConnectMessage*)message +{ + + // イベントの取得 + NSArray *evts = [super.eventMgr eventListForServiceId:DPHostDevicePluginServiceId + profile:DConnectMediaPlayerProfileName + attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; + + NSURL *contentURL = self.viewController.moviePlayer.contentURL; + if (contentURL) { + DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithURL:contentURL]; + if (mediaCtx.mediaId) { + [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:message]; + + } + if (mediaCtx.mimeType) { + [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:message]; + } + + [DConnectMediaPlayerProfile setPos:_viewController.moviePlayer.currentPlaybackTime target:message]; + } + + // イベント送信 + for (DConnectEvent *evt in evts) { + DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; + + [DConnectMediaPlayerProfile setMediaPlayer:message target:eventMsg]; + [SUPER_PLUGIN sendEvent:eventMsg]; + } +} + +- (MPMoviePlayerViewController *)viewControllerWithURL:(NSURL *)url +{ + MPMoviePlayerViewController *viewController = [[MPMoviePlayerViewController alloc] initWithContentURL:url]; + viewController.moviePlayer.shouldAutoplay = NO; + + // 再生完了通知をさせない様にする + // MPMoviePlayerViewControllerの初期動作だと、再生完了時に閉じる。 + // 再生完了時に閉じる処理を実行させたくないので、再生完了通知を一旦消す。 + [[NSNotificationCenter defaultCenter] removeObserver:self.viewController + name:MPMoviePlayerPlaybackDidFinishNotification + object:self.viewController.moviePlayer]; + // 再生完了の通知;独自の再生完了時に処理を行わせる。 + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(videoFinished:) + name:MPMoviePlayerPlaybackDidFinishNotification + object:self.viewController.moviePlayer]; + // 再生項目変更の通知 + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(nowPlayingItemChangedInMoviePlayer:) + name:MPMoviePlayerNowPlayingMovieDidChangeNotification + object:self.viewController.moviePlayer]; + + return viewController; +} + +#pragma mark - Movie Notification +- (void) videoFinished:(NSNotification*)notification +{ + + int value = [[notification.userInfo valueForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue]; + if (value == MPMovieFinishReasonUserExited) { + __weak DPHostMoviePlayer *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf.viewController.moviePlayer stop]; + [weakSelf.viewController dismissMoviePlayerViewControllerAnimated]; + weakSelf.viewController = nil; + [weakSelf nowPlayingItemChangedInMoviePlayer:notification]; + + // 一度閉じたら次回動画再生時には + // 別のMPMoviePlayerViewControllerインスタンスを使うので、 + // オブザーバーを削除しておく。 + [[NSNotificationCenter defaultCenter] removeObserver:weakSelf]; + }); + } + DConnectMessage *mediaPlayer = [DConnectMessage message]; + NSString *status = DConnectMediaPlayerProfileStatusComplete; + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + [self sendEventMovieWithMessage:mediaPlayer]; + +} + +- (void) nowPlayingItemChangedInMoviePlayer:(NSNotification *)notification +{ + // イベントの取得 + NSArray *evts = [super.eventMgr eventListForServiceId:DPHostDevicePluginServiceId + profile:DConnectMediaPlayerProfileName + attribute:DConnectMediaPlayerProfileAttrOnStatusChange]; + + DConnectMessage *mediaPlayer = [DConnectMessage message]; + + // 再生コンテンツ変更 + NSString *status; + NSURL *contentURL = [notification.object contentURL]; + MPMoviePlaybackState playbackState = self.viewController.moviePlayer.playbackState; + switch (playbackState) { + case MPMoviePlaybackStateStopped: + status = DConnectMediaPlayerProfileStatusStop; + break; + case MPMoviePlaybackStatePlaying: + status = DConnectMediaPlayerProfileStatusPlay; + break; + case MPMoviePlaybackStatePaused: + status = DConnectMediaPlayerProfileStatusPause; + break; + default: + status = DConnectMediaPlayerProfileStatusMedia; + break; + } + + [DConnectMediaPlayerProfile setStatus:status target:mediaPlayer]; + + if (contentURL) { + DPHostMediaContext *mediaCtx = [DPHostMediaContext contextWithURL:contentURL]; + if (mediaCtx) { + if (mediaCtx.mediaId) { + [DConnectMediaPlayerProfile setMediaId:mediaCtx.mediaId target:mediaPlayer]; + } + if (mediaCtx.mimeType) { + [DConnectMediaPlayerProfile setMIMEType:mediaCtx.mimeType target:mediaPlayer]; + } + } + + [DConnectMediaPlayerProfile setPos:self.viewController.moviePlayer.currentPlaybackTime target:mediaPlayer]; + } + + // イベント送信 + for (DConnectEvent *evt in evts) { + DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; + + [DConnectMediaPlayerProfile setMediaPlayer:mediaPlayer target:eventMsg]; + + [SUPER_PLUGIN sendEvent:eventMsg]; + } +} +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostNotificationProfile.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostNotificationProfile.m index 88235c03..c702e1b9 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostNotificationProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostNotificationProfile.m @@ -342,7 +342,7 @@ - (void) sendOnCloseEventWithNotificaitonId:(NSString *)notificationId #pragma mark - Notification Delegate - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)())completionHandler { + withCompletionHandler:(void (^)(void))completionHandler { completionHandler(); [self sendOnClickEventWithNotificaitonId:response.notification.request.identifier]; } From 4c58f1c73c3830aaa7ee321ffa4f77b8272e1e16 Mon Sep 17 00:00:00 2001 From: TakayukiHoshi1984 Date: Mon, 16 Oct 2017 09:29:09 +0900 Subject: [PATCH 02/13] =?UTF-8?q?MediaStreamRecordingProfile=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 6 + .../dConnectDeviceHost/Classes/DPHostUtils.h | 9 +- .../dConnectDeviceHost/Classes/DPHostUtils.m | 28 + .../Players/DPHostIPodAudioPlayer.m | 12 +- .../Players/DPHostMediaPlayer.h | 6 - .../Players/DPHostMediaPlayer.m | 28 - .../Players/DPHostMediaPlayerFactory.m | 18 +- .../Players/DPHostMoviePlayer.m | 32 +- .../DPHostMediaStreamRecordingProfile.h | 3 +- .../DPHostMediaStreamRecordingProfile.m | 1720 +---------------- .../DPHostRecorderContext.h | 17 +- .../DPHostRecorderContext.m | 20 +- .../DPHostRecorderManager.h | 50 + .../DPHostRecorderManager.m | 1385 +++++++++++++ 14 files changed, 1608 insertions(+), 1726 deletions(-) create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj index fabadc21..e8ba0fef 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ ABA43AC41EE562ED0019FE35 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABA43AC31EE562ED0019FE35 /* UserNotifications.framework */; }; ABDA3C6D1A0C58F3001F30A0 /* DPHostConnectionProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */; }; ABDA3C701A0C743A001F30A0 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABDA3C6F1A0C743A001F30A0 /* CoreBluetooth.framework */; }; + ABED44AB1F92396B00598460 /* DPHostRecorderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = ABED44AA1F92396B00598460 /* DPHostRecorderManager.m */; }; D62F0D1A1AA6E2CB00BC0ADA /* close_small_button_focus.png in Resources */ = {isa = PBXBuildFile; fileRef = D62F0D171AA6E2CB00BC0ADA /* close_small_button_focus.png */; }; D62F0D1B1AA6E2CB00BC0ADA /* close_small_button_normal.png in Resources */ = {isa = PBXBuildFile; fileRef = D62F0D181AA6E2CB00BC0ADA /* close_small_button_normal.png */; }; D62F0D1C1AA6E2CB00BC0ADA /* close_small_button_push.png in Resources */ = {isa = PBXBuildFile; fileRef = D62F0D191AA6E2CB00BC0ADA /* close_small_button_push.png */; }; @@ -167,6 +168,8 @@ ABDA3C6B1A0C58F3001F30A0 /* DPHostConnectionProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DPHostConnectionProfile.h; sourceTree = ""; }; ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPHostConnectionProfile.m; sourceTree = ""; }; ABDA3C6F1A0C743A001F30A0 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; }; + ABED44A91F92396B00598460 /* DPHostRecorderManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostRecorderManager.h; sourceTree = ""; }; + ABED44AA1F92396B00598460 /* DPHostRecorderManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostRecorderManager.m; sourceTree = ""; }; D62F0D171AA6E2CB00BC0ADA /* close_small_button_focus.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = close_small_button_focus.png; path = dConnectDeviceHost_resources/close_small_button_focus.png; sourceTree = SOURCE_ROOT; }; D62F0D181AA6E2CB00BC0ADA /* close_small_button_normal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = close_small_button_normal.png; path = dConnectDeviceHost_resources/close_small_button_normal.png; sourceTree = SOURCE_ROOT; }; D62F0D191AA6E2CB00BC0ADA /* close_small_button_push.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = close_small_button_push.png; path = dConnectDeviceHost_resources/close_small_button_push.png; sourceTree = SOURCE_ROOT; }; @@ -246,6 +249,8 @@ 25C710EA1981E578004C6484 /* DPHostMediaStreamRecordingProfile.m */, 25B42C6319865F1000B65E83 /* DPHostRecorderContext.h */, 25B42C6419865F1000B65E83 /* DPHostRecorderContext.m */, + ABED44A91F92396B00598460 /* DPHostRecorderManager.h */, + ABED44AA1F92396B00598460 /* DPHostRecorderManager.m */, ); path = DPHostMediaStreamRecording; sourceTree = ""; @@ -678,6 +683,7 @@ 25B42C6519865F1000B65E83 /* DPHostRecorderContext.m in Sources */, 25FF1DB4196A89AD00EA7F72 /* DPHostUtils.m in Sources */, 25FF0C0E196FBECF00E8C7C3 /* DPHostProximityProfile.m in Sources */, + ABED44AB1F92396B00598460 /* DPHostRecorderManager.m in Sources */, 25DF80D9196CE622003F6FF3 /* DPHostVibrationProfile.m in Sources */, D699974B1A7F264900EF934F /* DPHostCanvasDrawImage.m in Sources */, 257FC23719BAAF5200358536 /* DPHostReachability.m in Sources */, diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h index 81947716..f85e9570 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.h @@ -8,7 +8,7 @@ // #import - +#import #define SELF_PLUGIN ((DPHostDevicePlugin *)self.plugin) #define WEAKSELF_PLUGIN ((DPHostDevicePlugin *)weakSelf.plugin) #define SUPER_PLUGIN ((DPHostDevicePlugin *)super.plugin) @@ -22,4 +22,11 @@ + (BOOL)existDecimalWithString:(NSString*)decimal; + (BOOL)existCSVWithString:(NSString *)csv; +//NSErrorを生成 ++ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString*)message; + +// TOPにあるViewControllerを返す ++ (UIViewController*)topViewController; ++ (UIViewController*)topViewController:(UIViewController*)rootViewController; + @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.m index 3591ad53..bf1e9f0d 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/DPHostUtils.m @@ -15,6 +15,8 @@ static NSString * const kDPHostRegexDecimalPoint = @"^[-+]?([0-9]*)?(\\.)?([0-9]*)?$"; static NSString * const kDPHostRegexDigit = @"^([0-9]*)?$"; static NSString * const kDPHostRegexCSV = @"^([^,]*,)+"; +// Error Domain +static NSString *const kDPHostMediaPlayerErrorDomain = @"org.deviceconnect.ios.deviceplugin.host.mediaplayer.error"; @implementation DPHostUtils @@ -66,4 +68,30 @@ + (BOOL)existCSVWithString:(NSString *)csv { return [self existNumberWithString:csv Regex:kDPHostRegexCSV]; } ++ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString *)message +{ + NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; + errorDetail[NSLocalizedDescriptionKey] = message; + return [NSError errorWithDomain:kDPHostMediaPlayerErrorDomain code:code userInfo:errorDetail.mutableCopy].copy; +} + ++ (UIViewController*)topViewController +{ + return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController]; +} + ++ (UIViewController*)topViewController:(UIViewController *)rootViewController +{ + if (!rootViewController.presentedViewController) { + return rootViewController; + } + if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController*) rootViewController.presentedViewController; + UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; + return [self topViewController:lastViewController]; + } + + UIViewController *presentedViewController = (UIViewController*) rootViewController.presentedViewController; + return [self topViewController:presentedViewController]; +} @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m index 243b7765..56af51eb 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostIPodAudioPlayer.m @@ -39,7 +39,7 @@ - (instancetype)initWithMediaContext:(DPHostMediaContext *)ctx plugin:(DPHostDev [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; NSArray *items = [mediaQuery items]; if (items.count == 0) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found"]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found"]; return nil; } self.currentItem = items[0]; @@ -86,7 +86,7 @@ - (DPHostPlayerBlock)playWithError:(NSError **)error // (つまりiOSデバイス側にまだダウンロードされていない)で、 // 尚かつインターネット接続が無い場合は // 再生できない。 - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Internet is not reachable; the specified audio media item " "is an iClould item and its playback requires an Internet connection."]; return block; @@ -154,7 +154,7 @@ - (DPHostPlayerBlock)pauseWithError:(NSError **)error [weakSelf.musicPlayer pause]; }; } else { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; } return block; } @@ -174,7 +174,7 @@ - (DPHostPlayerBlock)resumeWithError:(NSError **)error //(つまりiOSデバイス側にまだダウンロードされていない)で、 //尚かつインターネット接続が無い場合は // 再生できない。 - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Internet is not reachable;" " the specified audio media item is an iClould " "item and its playback requires an Internet connection."]; @@ -190,7 +190,7 @@ - (DPHostPlayerBlock)resumeWithError:(NSError **)error [weakSelf.musicPlayer play]; }; } else { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; } return block; } @@ -206,7 +206,7 @@ - (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error MPMediaItem *nowPlayingItem = self.musicPlayer.nowPlayingItem; NSNumber *playbackDuration = [nowPlayingItem valueForProperty:MPMediaItemPropertyPlaybackDuration]; if ([playbackDuration unsignedIntegerValue] < [position unsignedIntegerValue]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"pos exceeds the playback duration."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"pos exceeds the playback duration."]; return block; } __weak DPHostIPodAudioPlayer *weakSelf = self; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h index 810b56c7..bdc2858e 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.h @@ -59,12 +59,6 @@ typedef void (^DPHostPlayerBlock)(void); // PUT /mediaPlayer/seek - (DPHostPlayerBlock)seekPosition:(NSNumber*)position error:(NSError**)error; -//NSErrorを生成 -+ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString*)message; - -// TOPにあるViewControllerを返す -+ (UIViewController*)topViewController; -+ (UIViewController*)topViewController:(UIViewController*)rootViewController; @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m index 56f2db5c..63d2a3c5 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayer.m @@ -10,8 +10,6 @@ #import "DPHostMediaPlayer.h" -// Error Domain -static NSString *const kDPHostMediaPlayerErrorDomain = @"org.deviceconnect.ios.deviceplugin.host.mediaplayer.error"; @implementation DPHostMediaPlayer @@ -69,31 +67,5 @@ - (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error return nil; } -#pragma mark - Utils method -+ (NSError*)throwsErrorCode:(NSInteger)code message:(NSString *)message -{ - NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; - errorDetail[NSLocalizedDescriptionKey] = message; - return [NSError errorWithDomain:kDPHostMediaPlayerErrorDomain code:code userInfo:errorDetail.mutableCopy].copy; -} - -+ (UIViewController*)topViewController -{ - return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController]; -} -+ (UIViewController*)topViewController:(UIViewController *)rootViewController -{ - if (!rootViewController.presentedViewController) { - return rootViewController; - } - if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) { - UINavigationController *navigationController = (UINavigationController*) rootViewController.presentedViewController; - UIViewController *lastViewController = [[navigationController viewControllers] lastObject]; - return [self topViewController:lastViewController]; - } - - UIViewController *presentedViewController = (UIViewController*) rootViewController.presentedViewController; - return [self topViewController:presentedViewController]; -} @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m index dba45abc..a6f42e3c 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMediaPlayerFactory.m @@ -23,7 +23,7 @@ + (DPHostMediaPlayer*)createPlayerWithMediaId:(NSString *)mediaId plugin:(DPHost DPHostMediaPlayer *player = nil; DPHostMediaContext *ctx = [DPHostMediaContext contextWithURL:url]; if (!ctx) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"MediaId is Invalid"]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"MediaId is Invalid"]; return nil; } BOOL isIPodAudioMedia = [url.scheme isEqualToString:MediaContextMediaIdSchemeIPodAudio]; @@ -63,22 +63,22 @@ + (NSArray*)searchMediaWithQuery:(NSString *)query } if (offset && offset.integerValue < 0) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"offset must be a non-negative value."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"offset must be a non-negative value."]; return nil; } if (limit && limit.integerValue < 0) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"limit must be a positive value."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"limit must be a positive value."]; return nil; } NSString *limitString = [limit stringValue]; NSString *offsetString = [offset stringValue]; if (![DPHostUtils existDigitWithString:limitString]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"limit must be a digit number."]; return nil; } if (![DPHostUtils existDigitWithString:offsetString]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"offset must be a digit number."]; return nil; } @@ -86,7 +86,7 @@ + (NSArray*)searchMediaWithQuery:(NSString *)query [ctxArr addObjectsFromArray:[self contextsBySearchingAssetsLibraryWithQuery:query mimeType:mimeType]]; [ctxArr addObjectsFromArray:[self contextsBySearchingIPodLibraryWithQuery:query mimeType:mimeType]]; if (offset && offset.integerValue >= ctxArr.count) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"offset exceeds the size of the media list."]; return nil; } @@ -265,7 +265,7 @@ + (void)checkOrder:(NSString **)sortOrder *sortOrder = order[1]; } if (!(*sortTarget) || !(*sortOrder)) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; } } else { *sortTarget = DConnectMediaPlayerProfileParamTitle; @@ -338,7 +338,7 @@ + (void)compareOrderWithSortTarget:(NSString *)sortTarget return [obj1 localizedCaseInsensitiveCompare: obj2]; }; } else { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; } @@ -357,7 +357,7 @@ + (void)compareOrderWithSortTarget:(NSString *)sortTarget }; } else if (![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderASC] && ![sortOrder isEqualToString:DConnectMediaPlayerProfileOrderDESC]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"order is invalid."]; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m index cf40f36d..0d4d6474 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaPlayer/Players/DPHostMoviePlayer.m @@ -39,7 +39,7 @@ - (instancetype)initWithMediaContext:(DPHostMediaContext *)ctx plugin:(DPHostDev [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; NSArray *items = [mediaQuery items]; if (items.count == 0) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found."]; return nil; } mediaItem = items[0]; @@ -84,7 +84,7 @@ - (DPHostPlayerBlock)playWithError:(NSError **)error [mediaQuery addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:persistentId forProperty:MPMediaItemPropertyPersistentID]]; NSArray *items = [mediaQuery items]; if (items.count == 0) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"Media specified by mediaId does not found."]; return block; } @@ -107,7 +107,7 @@ - (DPHostPlayerBlock)playWithError:(NSError **)error }; return block; } - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be played; media is not specified."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be played; media is not specified."]; } return block; } @@ -119,7 +119,7 @@ - (DPHostPlayerBlock)stopWithError:(NSError **)error return block; } if (![self moviePlayerViewControllerIsPresented]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Movie player view controller is not presented;" "please perform Media PUT API first to present the view controller."]; } else { @@ -142,7 +142,7 @@ - (DPHostPlayerBlock)pauseWithError:(NSError **)error { DPHostPlayerBlock block = ^{}; if (![self moviePlayerViewControllerIsPresented]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Movie player view controller is not presented;" " please perform Media PUT API first to present the view controller."]; } else { @@ -156,7 +156,7 @@ - (DPHostPlayerBlock)pauseWithError:(NSError **)error [weakSelf.viewController.moviePlayer pause]; }; } else { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannnot be paused; media is not playing."]; } } return block; @@ -166,7 +166,7 @@ - (DPHostPlayerBlock)resumeWithError:(NSError **)error { DPHostPlayerBlock block = ^{}; if (![self moviePlayerViewControllerIsPresented]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Movie player view controller is not presented;" "please perform Media PUT API first to present the view controller."]; } else { @@ -180,7 +180,7 @@ - (DPHostPlayerBlock)resumeWithError:(NSError **)error [weakSelf.viewController.moviePlayer play]; }; } else { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Media cannot be resumed; media is not paused."]; } } return block; @@ -189,7 +189,7 @@ - (DPHostPlayerBlock)resumeWithError:(NSError **)error - (NSTimeInterval)seekStatusWithError:(NSError **)error { if (!self.viewController) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Movie player view controller is not presented;" "please perform Media PUT API first to present the view controller."]; return -1.0f; @@ -201,14 +201,14 @@ - (DPHostPlayerBlock)seekPosition:(NSNumber *)position error:(NSError **)error { DPHostPlayerBlock block = ^{}; if (![self moviePlayerViewControllerIsPresented]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Movie player view controller is not presented; " "please perform Media PUT API first to present the view controller."]; return block; } NSTimeInterval playbackDuration = self.viewController.moviePlayer.duration; if (playbackDuration < [position unsignedIntegerValue]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"pos exceeds the playback duration."]; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"pos exceeds the playback duration."]; return block; } __weak DPHostMoviePlayer *weakSelf = self; @@ -235,7 +235,7 @@ - (void)setIpodMovieMediaWithUrl:(NSURL *)url if (isIPodMovieMedia) { NSNumber *isCloudItem = [mediaItem valueForProperty:MPMediaItemPropertyIsCloudItem]; if ([isCloudItem boolValue]) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Media item specified is an iTunes movie item," "and it must be downloaded into the iOS device before playing."]; return; @@ -245,7 +245,7 @@ - (void)setIpodMovieMediaWithUrl:(NSURL *)url // MoviePlayerではメディアのURLが必要なので、MPMediaItemからAssetURLを取得する。 movieURL = [mediaItem valueForProperty:MPMediaItemPropertyAssetURL]; if (!movieURL) { - *error = [DPHostMediaPlayer throwsErrorCode:DConnectMessageErrorCodeUnknown message: + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: @"Failed to pass the specified media item to the movie player;" "perhaps this media item is a protected media only playable " "in the official apps like \"Music\" and \"Videos\"."]; @@ -275,7 +275,7 @@ - (void)setIpodMovieMediaWithUrl:(NSURL *)url weakSelf.viewController = [weakSelf viewControllerWithURL:movieURL]; self.viewController.moviePlayer.shouldAutoplay = YES; block(); - [[DPHostMediaPlayer topViewController] presentMoviePlayerViewControllerAnimated:weakSelf.viewController]; + [[DPHostUtils topViewController] presentMoviePlayerViewControllerAnimated:weakSelf.viewController]; }); } dispatch_semaphore_signal(semaphore); @@ -288,8 +288,8 @@ - (BOOL)moviePlayerViewControllerIsPresented dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3); __block UIViewController *rootView = nil; dispatch_async(dispatch_get_main_queue(), ^{ - rootView = [DPHostMediaPlayer topViewController]; - [DPHostMediaPlayer topViewController:rootView]; + rootView = [DPHostUtils topViewController]; + [DPHostUtils topViewController:rootView]; dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, timeout); diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.h index 49b1b864..2264b6ad 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.h @@ -12,7 +12,6 @@ @interface DPHostMediaStreamRecordingProfile : DConnectMediaStreamRecordingProfile -/// @brief イベントマネージャ -@property DConnectEventManager *eventMgr; + @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m index 860c8253..92d8949a 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m @@ -16,135 +16,13 @@ #import "DPHostRecorderContext.h" #import "DPHostUtils.h" -NSUInteger MediaIdLength = 3; -static NSString *KeyPathAdjustingFocus = @"adjustingFocus"; -static NSString *KeyPathAdjustingExposure = @"adjustingExposure"; -static NSString *KeyPathAdjustingWhiteBalance = @"adjustingWhitBalance"; +#import "DPHostRecorderManager.h" -typedef NS_ENUM(NSUInteger, OptionIndex) { - OptionIndexImageWidth, ///< imageWidth: 画像の横幅 - OptionIndexImageHeight, ///< imageHeight: 画像の縦幅 - OptionIndexMimeType, ///< mimeType: MIMEタイプ -}; @interface DPHostMediaStreamRecordingProfile () - -@property NSDictionary *cameraInfoDict; -/*! - デフォルトの静止画レコーダーのID - iOSデバイスによっては背面カメラが無かったりと差異があるので、 - ランタイム時にデフォルトのレコーダーを決定する処理を行う。 - */ -@property (nonatomic) NSNumber *defaultPhotoRecorderId; -/*! - デフォルトの動画レコーダーのID - iOSデバイスによっては背面カメラが無かったりと差異があるので、 - ランタイム時にデフォルトのレコーダーを決定する処理を行う。 - */ -@property (nonatomic) NSNumber *defaultVideoRecorderId; -/*! - デフォルトの音声レコーダーのID - */ -@property (nonatomic) NSNumber *defaultAudioRecorderId; -/*! - カレントのレコーダーのID - */ -@property (nonatomic) NSNumber *currentRecorderId; - - -/// レコーダーで使用できる静止画入力データ -@property (nonatomic) NSMutableArray *photoDataSourceArr; -/// レコーダーで使用できる動画入力データ -@property (nonatomic) NSMutableArray *audioDataSourceArr; -/// レコーダーで使用できる音声入力データ -@property (nonatomic) NSMutableArray *videoDataSourceArr; - -/// 使用できるレコーダー -@property (nonatomic) NSMutableArray *recorderArr; - -@property ALAssetsLibrary *library; - - -/*! - @brief iOSデバイスの向き - 画面が天井や地面を向いた際は、無視して以前の向き情報を保持する。 - UIDeviceOrientationPortraitUpsideDown: - この場合、iOSデバイスを正面に見据えて、デバイスを反時計回りに180°回転し、 - Homeボタンが上方向にある状態。 - UIDeviceOrientationLandscapeLeft: - この場合、iOSデバイスを正面に見据えて、デバイスを反時計回りに90°回転し、 - Homeボタンが右方向にある状態。 - */ -@property (nonatomic) UIDeviceOrientation referenceOrientation; - -/// 前回プレビューを送った時間。 -@property (nonatomic) CMTime lastPreviewTimestamp; -/// Data Available Event APIでプレビュー画像URIの配送を行うかどうか。 -@property (nonatomic) BOOL sendPreview; -/// Data Available Event APIでプレビュー画像URIの配送を行うインターバル(秒)。 -@property (nonatomic) CMTime secPerFrame; - -/// ポーズ前最後のサンプルのタイムスタンプ -@property CMTime lastSampleTimestamp; -/// ポーズの累計期間 -@property CMTime totalPauseDuration; -/// ポーズの累計期間を再計算する必要が有るかどうか -@property BOOL needRecalculationOfTotalPauseDuration; -/** - @brief 現在のプレビュー画像の連番。 - Data Available Event APIで送るプレビュー画像は0-99までの連番を組み込んだ固定名を与えるの - で、現在0-99までのどの連番を使ったかを管理する。 - */ -@property int curPreviewImageEnumerator; - -/*! - オーディオAssetWriterInputを準備し、指定されたアセットライターに追加する。 - @param assetWriter オーディオAssetWriterInputを追加したいアセットライター - @param currentFormatDescription メディア情報を保持したオブジェクト - @retval YES assetWriterへのオーディオAssetWriterInput追加に成功。 - @retval NO assetWriterへのオーディオAssetWriterInput追加に失敗。 - */ -- (BOOL) setupAssetWriterAudioInputForRecorderContext:(DPHostRecorderContext *)recorderCtx - description:(CMFormatDescriptionRef)currentFormatDescription; -/*! - ビデオAssetWriterInputを準備し、指定されたアセットライターに追加する。 - @param assetWriter ビデオAssetWriterInputを追加したい - @param currentFormatDescription メディア情報を保持したオブジェクト - @retval YES assetWriterへのビデオAssetWriterInput追加に成功。 - @retval NO assetWriterへのビデオAssetWriterInput追加に失敗。 - */ -- (BOOL) setupAssetWriterVideoInputForRecorderContext:(DPHostRecorderContext *)recorderCtx - description:(CMFormatDescriptionRef)currentFormatDescription; - -- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(UIDeviceOrientation)orientation - position:(AVCaptureDevicePosition)position; - -- (CGAffineTransform)transformVideoOrientation:(AVCaptureVideoOrientation)orientation - position:(AVCaptureDevicePosition)position; - -- (void) sendOnPhotoEventWithPath:(NSString *)path mimeType:(NSString*)mimeType; -- (void) sendOnRecordingChangeEventWithStatus:(NSString *)status - path:(NSString *)path - mimeType:(NSString *)mimeType - errorMessage:(NSString *)errorMsg; -- (void) sendOnDataAvailableEventWithSampleBuffer:(CMSampleBufferRef)sampleBuffer; - -/*! - サンプルデータを書き込む。 - @param sampleBuffer オーディオもしくはビデオのサンプルデータ - @param recorderCtx AssetWriterInputを保持するレコーダー管理オブジェクト - @param isAudio YESならばオーディオAssetWriterInputに、 - NOならばビデオAssetWriterInputに - sampleBufferを追加 - @retval YES sampleBufferのAssetWriterInputへの書き込みに成功。 - @retval NO sampleBufferのAssetWriterInputへの書き込みに失敗。 - */ -- (BOOL) appendSampleBuffer:(CMSampleBufferRef)sampleBuffer - recorderContext:(DPHostRecorderContext *)recorderCtx isAudio:(BOOL)isAudio; - -- (CMSampleBufferRef) sampleBufferByAdjustingTimestamp:(CMSampleBufferRef)sample by:(CMTime)offset; - +@property DPHostRecorderManager *manager; +@property DConnectEventManager *eventMgr; @end @implementation DPHostMediaStreamRecordingProfile @@ -156,194 +34,7 @@ - (instancetype)init self.delegate = self; self.eventMgr = [DConnectEventManager sharedManagerForClass:[DPHostDevicePlugin class]]; __weak DPHostMediaStreamRecordingProfile *weakSelf = self; - - self.recorderArr = [NSMutableArray array]; - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - [notificationCenter addObserver:self selector:@selector(deviceOrientationDidChange) - name:UIDeviceOrientationDidChangeNotification object:nil]; - [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; - self.curPreviewImageEnumerator = 0; - self.currentRecorderId = nil; - self.secPerFrame = CMTimeMake(2, 1000); - self.lastPreviewTimestamp = kCMTimeInvalid; - self.lastSampleTimestamp = kCMTimeInvalid; - self.totalPauseDuration = kCMTimeInvalid; - self.needRecalculationOfTotalPauseDuration = NO; - DPHostRecorderDataSource *recCtx; - self.library = [ALAssetsLibrary new]; - AVCaptureSession *session; - self.photoDataSourceArr = [NSMutableArray array]; - self.audioDataSourceArr = [NSMutableArray array]; - self.videoDataSourceArr = [NSMutableArray array]; - NSString *defaultVideoDevUId = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo].uniqueID; - NSArray *audioDevArr = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; - NSArray *videoDevArr = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; - - for (AVCaptureDevice *audioDev in audioDevArr) { - // 出力:音声 - session = [AVCaptureSession new]; - recCtx = [DPHostRecorderDataSource recorderDataSourceForAudioWithAudioDevice:audioDev]; - - if (recCtx) { - recCtx.position = audioDev.position; - [self.audioDataSourceArr addObject:recCtx]; - } - } - for (AVCaptureDevice *videoDev in videoDevArr) { - session = [AVCaptureSession new]; - recCtx = [DPHostRecorderDataSource recorderDataSourceForPhotoWithVideoDevice:videoDev]; - - if (recCtx) { - recCtx.position = videoDev.position; - [self.photoDataSourceArr addObject:recCtx]; - } - session = [AVCaptureSession new]; - recCtx = [DPHostRecorderDataSource recorderDataSourceForVideoWithVideoDevice:videoDev]; - - if (recCtx) { - recCtx.position = videoDev.position; - NSMutableArray *dimensionArr = - @[ - AVCaptureSessionPreset352x288, - AVCaptureSessionPreset640x480, - AVCaptureSessionPreset1280x720, - AVCaptureSessionPreset1920x1080 - ].mutableCopy; - for (size_t i = 0; i < dimensionArr.count; ++i) { - if (![session canSetSessionPreset:dimensionArr[i]]) { - [dimensionArr removeObjectAtIndex:i]; - } - } - NSDictionary *(^getDimension)(NSString *) = ^ NSDictionary *(NSString *preset) { - if ([preset isEqualToString:AVCaptureSessionPreset352x288]) { - return @{@"h":@352 ,@"w":@288}; - } else if ([preset isEqualToString:AVCaptureSessionPreset640x480]) { - return @{@"h":@640 ,@"w":@480}; - } else if ([preset isEqualToString:AVCaptureSessionPreset1280x720]) { - return @{@"h":@1280 ,@"w":@720}; - } else if ([preset isEqualToString:AVCaptureSessionPreset1920x1080]) { - return @{@"h":@1920 ,@"w":@1080}; - } - return nil; - }; - NSDictionary *minDim = getDimension([dimensionArr firstObject]); - NSDictionary *maxDim = getDimension([dimensionArr lastObject]); - recCtx.imageHeight = recCtx.minImageHeight = minDim[@"h"]; - recCtx.imageWidth = recCtx.minImageWidth = minDim[@"w"]; - recCtx.maxImageHeight = maxDim[@"h"]; - recCtx.maxImageWidth = maxDim[@"w"]; - - [self.videoDataSourceArr addObject:recCtx]; - } - } - unsigned long videoNormalCount = 0; - unsigned long videoBackCount = 0; - unsigned long videoFrontCount = 0; - for (DPHostRecorderDataSource *dataSrc in self.photoDataSourceArr) { - if ([dataSrc.uniqueId isEqualToString:defaultVideoDevUId]) { - self.defaultPhotoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; - } - - DPHostRecorderContext *recorder = [[DPHostRecorderContext alloc] initWithProfile:self]; - recorder.type = RecorderTypePhoto; - recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"jpg"]; - recorder.state = RecorderStateInactive; - - [recorder setRecorderDataSource:dataSrc delegate:self]; - - NSMutableString *name = @"photo_".mutableCopy; - switch (dataSrc.position) { - case AVCaptureDevicePositionBack: - [name appendString:@"back_"]; - [name appendString:[NSString stringWithFormat:@"%lu", videoBackCount]]; - ++videoBackCount; - break; - case AVCaptureDevicePositionFront: - [name appendString:@"front_"]; - [name appendString:[NSString stringWithFormat:@"%lu", videoFrontCount]]; - ++videoFrontCount; - break; - case AVCaptureDevicePositionUnspecified: - default: - [name appendString:[NSString stringWithFormat:@"%lu", videoNormalCount]]; - ++videoNormalCount; - break; - } - recorder.name = [NSString stringWithString:name]; - - [self.recorderArr addObject:recorder]; - } - - unsigned long audioVideoNormalCount = 0; - unsigned long audioVideoBackCount = 0; - unsigned long audioVideoFrontCount = 0; - for (DPHostRecorderDataSource *videoDataSrc in self.videoDataSourceArr) { - // 動画(ビデオのみ) - if (self.audioDataSourceArr.count == 0 && [videoDataSrc.uniqueId isEqualToString:defaultVideoDevUId]) { - self.defaultVideoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; - } - DPHostRecorderContext *recorder; - NSMutableString *name; - - for (DPHostRecorderDataSource *audioDataSrc in self.audioDataSourceArr) { - // 動画(動画・音声) - if ([videoDataSrc.uniqueId isEqualToString:defaultVideoDevUId]) { - self.defaultVideoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; - } - - recorder = [[DPHostRecorderContext alloc] initWithProfile:self]; - recorder.type = RecorderTypeMovie; - recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"mp4"]; - recorder.state = RecorderStateInactive; - - [recorder setRecorderDataSource:audioDataSrc delegate:self]; - [recorder setRecorderDataSource:videoDataSrc delegate:self]; - - name = @"movie_audio_video_".mutableCopy; - switch (videoDataSrc.position) { - case AVCaptureDevicePositionBack: - [name appendString:@"back_"]; - [name appendString:[NSString stringWithFormat:@"%lu", audioVideoBackCount]]; - ++audioVideoBackCount; - break; - case AVCaptureDevicePositionFront: - [name appendString:@"front_"]; - [name appendString:[NSString stringWithFormat:@"%lu", audioVideoFrontCount]]; - ++audioVideoFrontCount; - break; - case AVCaptureDevicePositionUnspecified: - default: - [name appendString:[NSString stringWithFormat:@"%lu", audioVideoNormalCount]]; - ++audioVideoNormalCount; - break; - } - recorder.name = [NSString stringWithString:name]; - - [self.recorderArr addObject:recorder]; - } - } - - unsigned long audioCount = 0; - for (DPHostRecorderDataSource *audioDataSrc in self.audioDataSourceArr) { - DPHostRecorderContext *recorder; - NSMutableString *name; - - // 動画(音声のみ) - recorder = [[DPHostRecorderContext alloc] initWithProfile:self]; - recorder.type = RecorderTypeMovie; - recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"mp4"]; - recorder.state = RecorderStateInactive; - - [recorder setRecorderDataSource:audioDataSrc delegate:self]; - - name = @"movie_audio_".mutableCopy; - [name appendString:[NSString stringWithFormat:@"%lu", audioCount]]; - ++audioCount; - recorder.name = [NSString stringWithString:name]; - - [self.recorderArr addObject:recorder]; - } - self.defaultAudioRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count - 1]; + self.manager = [DPHostRecorderManager sharedManager]; // API登録(didReceiveGetMediaRecorderRequest相当) NSString *getPlayStatusRequestApiPath = [self apiPath: nil @@ -351,10 +42,9 @@ - (instancetype)init [self addGetPath: getPlayStatusRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { DConnectArray *recorders = [DConnectArray array]; - for (size_t i = 0; i < _recorderArr.count; ++i) { - DPHostRecorderContext *recorderItr = [weakSelf recorderArr][i]; - - + NSArray *recorderArr = [weakSelf.manager playStatus]; + for (size_t i = 0; i < recorderArr.count; ++i) { + DPHostRecorderContext *recorderItr = recorderArr[i]; [recorderItr performReading: ^{ DConnectMessage *recorder = [DConnectMessage message]; @@ -405,200 +95,18 @@ - (instancetype)init api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - - unsigned long long idx; - if (target || (target && target.length > 0)) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf defaultPhotoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [_defaultPhotoRecorderId unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - unsigned long long count = (unsigned)[weakSelf recorderArr].count; - - if (!_recorderArr || count < idx) { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - - DPHostRecorderContext *recorder; - @try { - recorder = _recorderArr[(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; + [weakSelf.manager takephotoForTarget:target completionHandler:^(NSURL *assetURL, NSError *error) { + if (!error) { + [DConnectMediaStreamRecordingProfile setUri:[assetURL absoluteString] target:response]; + [response setResult:DConnectMessageResultTypeOk]; + NSString *mimeType = [DConnectFileManager searchMimeTypeForExtension:assetURL.path.pathExtension]; + [weakSelf sendOnPhotoEventWithPath:[assetURL absoluteString] mimeType:mimeType]; } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - - for (DPHostRecorderContext *recorderItr in [weakSelf recorderArr]) { - if (recorderItr == recorder) { - continue; - } - if ((recorderItr.state == RecorderStateRecording) - && recorder.videoDevice && recorderItr.videoDevice && - [recorder.videoDevice.uniqueId isEqualToString:recorderItr.videoDevice.uniqueId]) { - // ビデオ入力デバイスが既に他のコンテキストで使われている。 - [response setErrorToUnknownWithMessage: - [NSString stringWithFormat:@"Video device is currently used by %@.", - recorderItr.name]]; - return YES; + [response setError:error.code message:error.localizedDescription]; } - } - - __block BOOL isSync = YES; - if (recorder.videoConnection.supportsVideoOrientation) { - recorder.videoConnection.videoOrientation = videoOrientationFromDeviceOrientation([UIDevice currentDevice].orientation); - } - [recorder performWriting: - ^{ - if (recorder.type != RecorderTypePhoto) { - [response setErrorToInvalidRequestParameterWithMessage: - @"target is not a video device; it is not capable of taking a photo."]; - isSync = YES; - return; - } - - if (![recorder.session isRunning]) { - [recorder.session startRunning]; - } - - // ライトが点いていたら消灯する。 - [weakSelf setLightOff]; - - // 写真を撮影する。 - __block AVCaptureDevice *captureDevice = [AVCaptureDevice deviceWithUniqueID:recorder.videoDevice.uniqueId]; - NSError *error; - [captureDevice lockForConfiguration:&error]; - - if (error) { - NSLog(@"Failed to acquire a configuration lock for %@.", captureDevice.uniqueID); - } else { - - if (captureDevice.focusMode != AVCaptureFocusModeContinuousAutoFocus && - [captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { - captureDevice.focusMode = AVCaptureFocusModeContinuousAutoFocus; - } else if (captureDevice.focusMode != AVCaptureFocusModeAutoFocus && - [captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { - captureDevice.focusMode = AVCaptureFocusModeAutoFocus; - } else if (captureDevice.focusMode != AVCaptureFocusModeLocked && - [captureDevice isFocusModeSupported:AVCaptureFocusModeLocked]) { - captureDevice.focusMode = AVCaptureFocusModeLocked; - } - if (captureDevice.exposureMode != AVCaptureExposureModeContinuousAutoExposure && - [captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - captureDevice.exposureMode = AVCaptureExposureModeContinuousAutoExposure; - } else if (captureDevice.exposureMode != AVCaptureExposureModeAutoExpose && - [captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) { - captureDevice.exposureMode = AVCaptureExposureModeAutoExpose; - } else if (captureDevice.exposureMode != AVCaptureExposureModeLocked && - [captureDevice isExposureModeSupported:AVCaptureExposureModeLocked]) { - captureDevice.exposureMode = AVCaptureExposureModeLocked; - } - if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance && - [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) { - captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance; - } else if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeAutoWhiteBalance && - [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) { - captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeAutoWhiteBalance; - } else if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeLocked && - [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) { - captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked; - } - if (captureDevice.automaticallyEnablesLowLightBoostWhenAvailable != NO && - captureDevice.lowLightBoostSupported) { - captureDevice.automaticallyEnablesLowLightBoostWhenAvailable = YES; - } - [captureDevice unlockForConfiguration]; - - [NSThread sleepForTimeInterval:0.5]; - } - - AVCaptureStillImageOutput *stillImageOutput = (AVCaptureStillImageOutput *)recorder.videoConnection.output; - [stillImageOutput captureStillImageAsynchronouslyFromConnection:recorder.videoConnection - completionHandler: - ^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { - if (!imageDataSampleBuffer || error) { - [response setErrorToUnknownWithMessage:@"Failed to take a photo."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - NSData *jpegData; - @try { - jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; - - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSInvalidArgumentException]) { - message = @"Non-JPEG data was given."; - } else { - message = [NSString stringWithFormat:@"%@ encountered.", [exception name]]; - } - [response setErrorToUnknownWithMessage:message]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - - // EXIF情報を水平に統一する。ブラウザによってはEXIF情報により画像の向きが変わるため。 - CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)jpegData, NULL); - NSDictionary *metadata = (__bridge NSDictionary*) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL); - NSMutableDictionary *meta = [NSMutableDictionary dictionaryWithDictionary:metadata]; - NSMutableDictionary *tiff = meta[(NSString*) kCGImagePropertyTIFFDictionary]; - tiff[(NSString*) kCGImagePropertyTIFFOrientation] = @(kCGImagePropertyOrientationUp); - meta[(NSString*) kCGImagePropertyTIFFDictionary] = tiff; - meta[(NSString*) kCGImagePropertyOrientation] = @(kCGImagePropertyOrientationUp); - UIImage *jpeg = [[UIImage alloc] initWithData:jpegData]; - UIImage *fixJpeg = [weakSelf fixOrientationWithImage:jpeg position:recorder.videoDevice.position]; - [[weakSelf library] writeImageToSavedPhotosAlbum:fixJpeg.CGImage metadata:meta completionBlock: - ^(NSURL *assetURL, NSError *error) { - if (!assetURL || error) { - [response setErrorToUnknownWithMessage:@"Failed to save a photo to camera roll."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - [DConnectMediaStreamRecordingProfile setUri:[assetURL absoluteString] target:response]; - [response setResult:DConnectMessageResultTypeOk]; - - if ([recorder.session isRunning]) { - [recorder.session stopRunning]; - } - - [[DConnectManager sharedManager] sendResponse:response]; - - NSString *mimeType = [DConnectFileManager searchMimeTypeForExtension:assetURL.path.pathExtension]; - [weakSelf sendOnPhotoEventWithPath:[assetURL absoluteString] mimeType:mimeType]; - - return; - }]; - }]; - - // 非同期の「- captureStillImageAsynchronouslyFromConnection:completionHandler:」の処理内 - // でHTTPレスポンスを返却させる。 - isSync = NO; - return; - }]; - - return isSync; + [[DConnectManager sharedManager] sendResponse:response]; + }]; + return NO; }]; // API登録(didReceivePostRecordRequest相当) @@ -617,219 +125,52 @@ - (instancetype)init @"timeslice is not supported; please omit this parameter."]; return YES; } - - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; + [weakSelf.manager recordForTarget:target timeSlice:timeslice response:response completionHandler:^(NSError *error) { + if (!error) { + [response setResult:DConnectMessageResultTypeOk]; + [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateRecording + path:nil mimeType:nil errorMessage:nil]; } else { - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - + [response setError:error.code message:error.localizedDescription]; } - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (!_recorderArr || _recorderArr.count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - _currentRecorderId = [NSNumber numberWithUnsignedLongLong:idx]; - DPHostRecorderContext *recorder; - @try { - recorder = _recorderArr[(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; - } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - - if (recorder.state == RecorderStateRecording) { - [response setErrorToIllegalDeviceStateWithMessage:@"target is already recording."]; - return YES; - } - - // 入力デバイスが既に他のレコーダーで使われていないかをチェックする - for (DPHostRecorderContext *recorderItr in [weakSelf recorderArr]) { - if (recorderItr == recorder) { - continue; - } - if (recorderItr.state == RecorderStateRecording) { - if (recorder.audioDevice && recorderItr.audioDevice && - [recorder.audioDevice.uniqueId isEqualToString:recorderItr.audioDevice.uniqueId]) { - // 音声入力デバイスが既に他のコンテキストで使われている。 - [response setErrorToUnknownWithMessage: - [NSString stringWithFormat:@"Audio device is currently used by %@.", - recorderItr.name]]; - return YES; - } - if (recorder.videoDevice && recorderItr.videoDevice && - [recorder.videoDevice.uniqueId isEqualToString:recorderItr.videoDevice.uniqueId]) { - // ビデオ入力デバイスが既に他のコンテキストで使われている。 - [response setErrorToUnknownWithMessage: - [NSString stringWithFormat:@"Video device is currently used by %@.", - recorderItr.name]]; - return YES; - } - } - } - - __block BOOL isSync = YES; - [recorder performWriting: - ^{ - if (recorder.type != RecorderTypeMovie) { - [response setErrorToInvalidRequestParameterWithMessage: - @"target is not an audiovisual device; it is not capable of taking a movie."]; - return; - } - - // ライトが点いていたら消灯する。 - [weakSelf setLightOff]; - - recorder.videoOrientation = [recorder.videoConnection videoOrientation]; - - AVCaptureDevice *captureDevice = [AVCaptureDevice deviceWithUniqueID:recorder.videoDevice.uniqueId]; - NSError *error; - [captureDevice lockForConfiguration:&error]; - if (error) { - NSLog(@"Failed to acquire a configuration lock for %@.", captureDevice.uniqueID); - } else { - - // 画面中央に露光やフォーカスが調整される様にする。 - CGPoint pointOfInterest = CGPointMake(.5, .5); - if ([captureDevice isFocusPointOfInterestSupported] && - [captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { - captureDevice.focusPointOfInterest = pointOfInterest; - captureDevice.focusMode = AVCaptureFocusModeAutoFocus; - } - if ([captureDevice isExposurePointOfInterestSupported] && - [captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - captureDevice.exposurePointOfInterest = pointOfInterest; - captureDevice.exposureMode = - AVCaptureExposureModeContinuousAutoExposure; - } - [captureDevice unlockForConfiguration]; - - // 露光の為に少し待つ - [NSThread sleepForTimeInterval:0.5]; - } - - [recorder setupAssetWriterWithResponse:response]; - recorder.state = RecorderStateRecording; - - // ポーズ関連の変数を初期化 - _lastPreviewTimestamp = kCMTimeInvalid; - _totalPauseDuration = kCMTimeInvalid; - _needRecalculationOfTotalPauseDuration = NO; - - if (![recorder.session isRunning]) { - [recorder.session startRunning]; - } - isSync = NO; - }]; - - return isSync; + [[DConnectManager sharedManager] sendResponse:response]; + }]; + return NO; }]; - - // API登録(didReceivePutPauseRequest相当) - NSString *putPauseRequestApiPath = [self apiPath: nil - attributeName: DConnectMediaStreamRecordingProfileAttrPause]; - [self addPutPath: putPauseRequestApiPath + // API登録(didReceivePutStopRequest相当) + NSString *putStopRequestApiPath = [self apiPath: nil + attributeName: DConnectMediaStreamRecordingProfileAttrStop]; + [self addPutPath: putStopRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - idx = [_currentRecorderId unsignedLongLongValue]; - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [[weakSelf defaultVideoRecorderId] unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (![weakSelf recorderArr] || [weakSelf recorderArr].count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - - [weakSelf setCurrentRecorderId: [NSNumber numberWithUnsignedLongLong:idx]]; - DPHostRecorderContext *recorder; - @try { - recorder = [weakSelf recorderArr][(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; + [weakSelf.manager stopForTarget:target completionHandler:^(NSURL *assetURL, NSError *error) { + if (!error) { + [DConnectMediaStreamRecordingProfile setUri:[assetURL absoluteString] target:response]; + [response setResult:DConnectMessageResultTypeOk]; + + [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateStop + path:[assetURL absoluteString] mimeType:nil + errorMessage:nil]; } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - - if (recorder.state == RecorderStatePaused) { - [response setErrorToIllegalDeviceStateWithMessage:@"target is already pausing."]; - return YES; - } - - if (recorder.state == RecorderStateRecording) { - if ([recorder.session isRunning]) { - [recorder.session stopRunning]; - if ([recorder.session isRunning]) { - [response setErrorToUnknownWithMessage: - @"Failed to pause the specified recorder; failed to stop capture session."]; - return YES; - } + [response setError:error.code message:error.localizedDescription]; } - recorder.state = RecorderStatePaused; - - [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStatePause - path:nil mimeType:nil errorMessage:nil]; + [[DConnectManager sharedManager] sendResponse:response]; - [weakSelf setNeedRecalculationOfTotalPauseDuration: YES]; - - [response setResult:DConnectMessageResultTypeOk]; - } else { - [response setErrorToIllegalDeviceStateWithMessage: - @"The specified recorder is not recording; no need for pause."]; - } + }]; + return NO; + }]; + // API登録(didReceivePutPauseRequest相当) + NSString *putPauseRequestApiPath = [self apiPath: nil + attributeName: DConnectMediaStreamRecordingProfileAttrPause]; + [self addPutPath: putPauseRequestApiPath + api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { + NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; + NSError *error = nil; + [weakSelf.manager pauseForTarget:target error:&error]; + [weakSelf runMediaStreamRecordingWithError:error response:response state:DConnectMediaStreamRecordingProfileRecordingStatePause]; return YES; }]; @@ -840,209 +181,13 @@ - (instancetype)init api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - idx = [_currentRecorderId unsignedLongLongValue]; - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [[weakSelf defaultVideoRecorderId] unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (![weakSelf recorderArr] || [weakSelf recorderArr].count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - [weakSelf setCurrentRecorderId: [NSNumber numberWithUnsignedLongLong:idx]]; - DPHostRecorderContext *recorder; - @try { - recorder = [weakSelf recorderArr][(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; - } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - if (recorder.state == RecorderStateRecording) { - [response setErrorToIllegalDeviceStateWithMessage:@"target is not pausing."]; - return YES; - } - if (recorder.state == RecorderStatePaused) { - if (![recorder.session isRunning]) { - [recorder.session startRunning]; - if (![recorder.session isRunning]) { - [response setErrorToUnknownWithMessage: - @"Failed to resume the specified recorder; failed to start capture session."]; - return YES; - } - } - recorder.state = RecorderStateRecording; - - [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateResume - path:nil mimeType:nil errorMessage:nil]; - [response setResult:DConnectMessageResultTypeOk]; - } else { - [response setErrorToIllegalDeviceStateWithMessage: - @"The specified recorder is not recording; no need for pause."]; - } - + NSError *error = nil; + [weakSelf.manager resumeForTarget:target error:&error]; + [weakSelf runMediaStreamRecordingWithError:error response:response state:DConnectMediaStreamRecordingProfileRecordingStateResume]; return YES; }]; - // API登録(didReceivePutStopRequest相当) - NSString *putStopRequestApiPath = [self apiPath: nil - attributeName: DConnectMediaStreamRecordingProfileAttrStop]; - [self addPutPath: putStopRequestApiPath - api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - idx = [_currentRecorderId unsignedLongLongValue]; - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (!_recorderArr || _recorderArr.count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - - _currentRecorderId = [NSNumber numberWithUnsignedLongLong:idx]; - DPHostRecorderContext *recorder; - @try { - recorder = _recorderArr[(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; - } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - if (recorder.state == RecorderStateInactive) { - [response setErrorToIllegalDeviceStateWithMessage:@"target is not recording."]; - return YES; - } - - [recorder performWriting: - ^{ - // レコーディングサンプルの配信を停止する。 - [recorder.session stopRunning]; - - if (recorder.audioWriterInput) { - if (recorder.writer.status != AVAssetWriterStatusUnknown) { - [recorder.audioWriterInput markAsFinished]; - } - } - if (recorder.videoWriterInput) { - if (recorder.writer.status != AVAssetWriterStatusUnknown) { - [recorder.videoWriterInput markAsFinished]; - } - } - - recorder.state = RecorderStateInactive; - recorder.audioReady = recorder.videoReady = NO; - }]; - - if (!recorder.writer) { - [response setErrorToIllegalDeviceStateWithMessage:@"Writer is non exist."]; - return YES; - } - if (recorder.writer.status == AVAssetWriterStatusUnknown) { - [response setErrorToIllegalDeviceStateWithMessage:@"Unknown Failed to finishing an aseet writer"]; - return YES; - } - - [recorder.writer finishWritingWithCompletionHandler: - ^{ - - if (recorder.writer.status == AVAssetWriterStatusFailed) { - [response setErrorToUnknownWithMessage:@"Failed to finishing an aseet writer"]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - NSURL *fileUrl = recorder.writer.outputURL; - - // 動画をカメラロールに追加。 - [[weakSelf library] writeVideoAtPathToSavedPhotosAlbum:fileUrl - completionBlock: - ^(NSURL *assetURL, NSError *error) { - if (error) { - [response setErrorToUnknownWithMessage:@"Failed to save a movie to camera roll (1)."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } else if (!assetURL) { - [response setErrorToUnknownWithMessage:@"Failed to save a movie to camera roll (2)."]; - [[DConnectManager sharedManager] sendResponse:response]; - return; - } - NSFileManager *fileMgr = [NSFileManager defaultManager]; - if ([fileMgr fileExistsAtPath:[fileUrl path]] - && ![fileMgr removeItemAtURL:fileUrl error:nil]) { - NSLog(@"Failed to remove a movie file."); - } - - [DConnectMediaStreamRecordingProfile setUri:[assetURL absoluteString] target:response]; - [response setResult:DConnectMessageResultTypeOk]; - - [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateStop - path:[assetURL absoluteString] mimeType:recorder.mimeType - errorMessage:nil]; - [[DConnectManager sharedManager] sendResponse:response]; - _currentRecorderId = nil; - recorder.writer = nil; - recorder.audioWriterInput = recorder.videoWriterInput = nil; - - }]; - }]; - - // 「- finishWritingWithCompletionHandler:」の中でHTTPレスポンスを返却させる - return NO; - }]; // API登録(didReceivePutMuteTrackRequest相当) NSString *putMuteTrackRequestApiPath = [self apiPath: nil @@ -1051,70 +196,9 @@ - (instancetype)init api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - idx = [_currentRecorderId unsignedLongLongValue]; - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (!_recorderArr || _recorderArr.count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - - _currentRecorderId = [NSNumber numberWithUnsignedLongLong:idx]; - DPHostRecorderContext *recorder; - @try { - recorder = _recorderArr[(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; - } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - - if (!recorder.audioDevice) { - [response setErrorToUnknownWithMessage: - @"The specified target does not capture audio and can not be muted."]; - return YES; - } - - if (!recorder.isMuted) { - recorder.isMuted = YES; - - [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateMutetrack - path:nil mimeType:nil errorMessage:nil]; - - [response setResult:DConnectMessageResultTypeOk]; - } else { - [response setErrorToIllegalDeviceStateWithMessage:@"The specified recorder is already muted."]; - } - + NSError *error = nil; + [weakSelf.manager muteTrackForTarget:target error:&error]; + [weakSelf runMediaStreamRecordingWithError:error response:response state:DConnectMediaStreamRecordingProfileRecordingStateMutetrack]; return YES; }]; @@ -1126,68 +210,11 @@ - (instancetype)init NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - unsigned long long idx; - if (target) { - if ([target isEqualToString:@"video"]) { - idx = [_defaultVideoRecorderId unsignedLongLongValue]; - } else if ([target isEqualToString:@"audio"]) { - idx = [_defaultAudioRecorderId unsignedLongLongValue]; - } else { - idx = [_currentRecorderId unsignedLongLongValue]; - BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; - if (!success) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - } - } else if ([weakSelf currentRecorderId]) { - idx = [[weakSelf currentRecorderId] unsignedLongLongValue]; - - } else if ([weakSelf defaultVideoRecorderId]) { - // target省略時はデフォルトのレコーダーを指定する。 - idx = [[weakSelf defaultVideoRecorderId] unsignedLongLongValue]; - } else { - [response setErrorToInvalidRequestParameterWithMessage: - @"target was not specified, and no default target was set; please specify an existing target."]; - return YES; - } - if (!_recorderArr || _recorderArr.count < idx) { - [response setErrorToInvalidRequestParameterWithMessage:@"target is invalid."]; - return YES; - } - _currentRecorderId = [NSNumber numberWithUnsignedLongLong:idx]; - DPHostRecorderContext *recorder; - @try { - recorder = _recorderArr[(NSUInteger)idx]; - } - @catch (NSException *exception) { - NSString *message; - if ([[exception name] isEqualToString:NSRangeException]) { - message = @"target is not found in the recorder ID list."; - } else { - message = @"Exception encountered while trying to access the recorder ID list."; - } - [response setErrorToInvalidRequestParameterWithMessage:message]; - return YES; - } - - if (!recorder.audioDevice) { - [response setErrorToUnknownWithMessage: - @"The specified target does not capture audio and can not be unmuted."]; - return YES; - } - - if (recorder.isMuted) { - recorder.isMuted = NO; - - [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateUnmutetrack - path:nil mimeType:nil errorMessage:nil]; - - [response setResult:DConnectMessageResultTypeOk]; - } else { - [response setErrorToIllegalDeviceStateWithMessage:@"The specified recorder is not muted."]; - } - + NSError *error = nil; + [weakSelf.manager unmuteTrackForTarget:target error:&error]; + [weakSelf runMediaStreamRecordingWithError:error + response:response + state:DConnectMediaStreamRecordingProfileRecordingStateUnmutetrack]; return YES; }]; @@ -1249,7 +276,7 @@ - (instancetype)init serviceId:serviceId target:nil timeslice:nil]; // プレビュー画像URIの配送処理が開始されていないのなら、開始する。 - _sendPreview = YES; +// _sendPreview = YES; } switch ([[weakSelf eventMgr] addEventForRequest:request]) { @@ -1339,9 +366,9 @@ - (instancetype)init serviceId:serviceId target:nil]; // イベント受領先が存在しないなら、プレビュー画像URIの配送処理を停止する。 - _sendPreview = NO; +// _sendPreview = NO; // 次回プレビュー開始時に影響を与えない為に、初期値(無効値)を設定する。 - _lastPreviewTimestamp = kCMTimeInvalid; +// _lastPreviewTimestamp = kCMTimeInvalid; } return YES; @@ -1359,155 +386,7 @@ - (void)dealloc [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; } -- (BOOL) setupAssetWriterAudioInputForRecorderContext:(DPHostRecorderContext *)recorderCtx - description:(CMFormatDescriptionRef)currentFormatDescription -{ - if (!recorderCtx.writer) { - NSLog(@"assetWriter must be specified."); - return NO; - } - - const AudioStreamBasicDescription *currentASBD - = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription); - - size_t aclSize = 0; - const AudioChannelLayout *currentChannelLayout - = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize); - NSData *currentChannelLayoutData = nil; - - if ( currentChannelLayout && aclSize > 0 ) - currentChannelLayoutData = [NSData dataWithBytes:currentChannelLayout length:aclSize]; - else - currentChannelLayoutData = [NSData data]; - - NSDictionary *audioCompressionSettings = - @{ - AVFormatIDKey : @(kAudioFormatMPEG4AAC), - AVSampleRateKey : @(currentASBD->mSampleRate), - AVEncoderBitRatePerChannelKey : @64000, - AVNumberOfChannelsKey : @(currentASBD->mChannelsPerFrame), - AVChannelLayoutKey : currentChannelLayoutData - }; - if ([recorderCtx.writer canApplyOutputSettings:audioCompressionSettings forMediaType:AVMediaTypeAudio]) { - AVAssetWriterInput *assetWriterAudioIn = recorderCtx.audioWriterInput - = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings]; - assetWriterAudioIn.expectsMediaDataInRealTime = YES; - - if ([recorderCtx.writer canAddInput:assetWriterAudioIn]) { - [recorderCtx.writer addInput:assetWriterAudioIn]; - recorderCtx.audioReady = YES; - } - else { - NSLog(@"Could not add asset writer audio input."); - return NO; - } - } - else { - NSLog(@"Could not apply audio output settings."); - return NO; - } - - return YES; -} - -- (BOOL) setupAssetWriterVideoInputForRecorderContext:(DPHostRecorderContext *)recorderCtx - description:(CMFormatDescriptionRef)currentFormatDescription -{ - if (!recorderCtx.writer) { - NSLog(@"assetWriter must be specified."); - return NO; - } - float bitsPerPixel; - CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription); - int numPixels = dimensions.width * dimensions.height; - int bitsPerSecond; - - // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate - if ( numPixels < (640 * 480) ) - bitsPerPixel = 4.05; // This bitrate matches the quality produced by AVCaptureSessionPresetMedium or Low. - else - bitsPerPixel = 11.4; // This bitrate matches the quality produced by AVCaptureSessionPresetHigh. - - bitsPerSecond = numPixels * bitsPerPixel; - - NSDictionary *videoCompressionSettings = - @{ - AVVideoCodecKey : AVVideoCodecH264, - AVVideoWidthKey : @(dimensions.width), - AVVideoHeightKey : @(dimensions.height), - AVVideoCompressionPropertiesKey : @{ - AVVideoAverageBitRateKey : @(bitsPerSecond), - AVVideoMaxKeyFrameIntervalKey : @30, - }, - }; - if ([recorderCtx.writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo]) { - AVAssetWriterInput *assetWriterVideoIn = recorderCtx.videoWriterInput - = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings]; - assetWriterVideoIn.expectsMediaDataInRealTime = YES; - - assetWriterVideoIn.transform = - [self transformVideoOrientation:recorderCtx.videoOrientation position:recorderCtx.videoDevice.position]; - if ([recorderCtx.writer canAddInput:assetWriterVideoIn]) { - [recorderCtx.writer addInput:assetWriterVideoIn]; - recorderCtx.videoReady = YES; - } - else { - NSLog(@"Couldn't add asset writer video input."); - return NO; - } - } - else { - NSLog(@"Couldn't apply video output settings."); - return NO; - } - - return YES; -} - -- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(UIDeviceOrientation)orientation - position:(AVCaptureDevicePosition)position -{ - CGFloat angle = 0.0; - - switch (orientation) { - case UIDeviceOrientationPortrait: - angle = 0.0; - break; - case UIDeviceOrientationPortraitUpsideDown: - angle = M_PI; - break; - case UIDeviceOrientationLandscapeLeft: - angle = position == AVCaptureDevicePositionBack ? -M_PI_2 : M_PI_2; - break; - case UIDeviceOrientationLandscapeRight: - angle = position == AVCaptureDevicePositionBack ? M_PI_2 : -M_PI_2; - break; - default: - break; - } - - return angle; -} - -- (CGAffineTransform)transformVideoOrientation:(AVCaptureVideoOrientation)orientation - position:(AVCaptureDevicePosition)position -{ - CGAffineTransform transform = CGAffineTransformIdentity; - - // iOSデバイスの向きが、ポートレート状態から角度的に何度の差があるか算出。 - CGFloat orientationAngleOffset = - [self angleOffsetFromPortraitOrientationToOrientation:_referenceOrientation - position:position]; - CGFloat videoOrientationAngleOffset = - [self angleOffsetFromPortraitOrientationToOrientation:(UIDeviceOrientation)orientation - position:position]; - - // Find the difference in angle between the passed in orientation and the current video orientation - CGFloat angleOffset = orientationAngleOffset - videoOrientationAngleOffset; - transform = CGAffineTransformMakeRotation(angleOffset); - - return transform; -} +#pragma mark - Send Event - (void) sendOnPhotoEventWithPath:(NSString *)path mimeType:(NSString*)mimeType { @@ -1568,49 +447,6 @@ - (void) sendOnRecordingChangeEventWithStatus:(NSString *)status } } -// Create a UIImage from sample buffer data -- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer -{ - // Get a CMSampleBuffer's Core Video image buffer for the media data - CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - // Lock the base address of the pixel buffer - CVPixelBufferLockBaseAddress(imageBuffer, 0); - - // Get the number of bytes per row for the pixel buffer - void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer); - - // Get the number of bytes per row for the pixel buffer - size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); - // Get the pixel buffer width and height - size_t width = CVPixelBufferGetWidth(imageBuffer); - size_t height = CVPixelBufferGetHeight(imageBuffer); - - // Create a device-dependent RGB color space - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - - // Create a bitmap graphics context with the sample buffer data - CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, - bytesPerRow, - colorSpace, - kCGBitmapByteOrder32Little - | kCGImageAlphaPremultipliedFirst); - // Create a Quartz image from the pixel data in the bitmap graphics context - CGImageRef quartzImage = CGBitmapContextCreateImage(context); - // Unlock the pixel buffer - CVPixelBufferUnlockBaseAddress(imageBuffer,0); - - // Free up the context and color space - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - - // Create an image object from the Quartz image - UIImage *image = [UIImage imageWithCGImage:quartzImage]; - - // Release the Quartz image - CGImageRelease(quartzImage); - - return (image); -} - (void) sendOnDataAvailableEventWithSampleBuffer:(CMSampleBufferRef)sampleBuffer { @@ -1643,16 +479,16 @@ - (void) sendOnDataAvailableEventWithSampleBuffer:(CMSampleBufferRef)sampleBuffe UIGraphicsEndImageContext(); NSData *jpegData = UIImageJPEGRepresentation(image, 1.0); - NSString *fileName = [NSString stringWithFormat:@"preview_%02d", _curPreviewImageEnumerator]; - fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - if (!fileURL || ![jpegData writeToURL:fileURL atomically:NO]) { - return; - } +// NSString *fileName = [NSString stringWithFormat:@"preview_%02d", _curPreviewImageEnumerator]; +// fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; +// if (!fileURL || ![jpegData writeToURL:fileURL atomically:NO]) { +// return; +// } } else { return; } } - _curPreviewImageEnumerator = (_curPreviewImageEnumerator + 1) % 100; +// _curPreviewImageEnumerator = (_curPreviewImageEnumerator + 1) % 100; DConnectURIBuilder *builder = [DConnectURIBuilder new]; builder.profile = @"files"; @@ -1674,408 +510,22 @@ - (void) sendOnDataAvailableEventWithSampleBuffer:(CMSampleBufferRef)sampleBuffe } } -AVCaptureVideoOrientation videoOrientationFromDeviceOrientation(UIDeviceOrientation deviceOrientation) -{ - AVCaptureVideoOrientation orientation; - switch (deviceOrientation) { - case UIDeviceOrientationUnknown: - orientation = AVCaptureVideoOrientationPortrait; - break; - case UIDeviceOrientationPortrait: - orientation = AVCaptureVideoOrientationPortrait; - break; - case UIDeviceOrientationPortraitUpsideDown: - orientation = AVCaptureVideoOrientationPortraitUpsideDown; - break; - case UIDeviceOrientationLandscapeLeft: - orientation = AVCaptureVideoOrientationLandscapeRight; - break; - case UIDeviceOrientationLandscapeRight: - orientation = AVCaptureVideoOrientationLandscapeLeft; - break; - case UIDeviceOrientationFaceUp: - orientation = AVCaptureVideoOrientationPortrait; - break; - case UIDeviceOrientationFaceDown: - orientation = AVCaptureVideoOrientationPortrait; - break; - } - return orientation; -} -- (UIImage *)fixOrientationWithImage:(UIImage *)image position:(AVCaptureDevicePosition) position{ - - if (image.imageOrientation == UIImageOrientationUp && position != AVCaptureDevicePositionFront) return image; - - CGAffineTransform transform = CGAffineTransformIdentity; - - switch (image.imageOrientation) { - case UIImageOrientationDown: - case UIImageOrientationDownMirrored: - transform = CGAffineTransformTranslate(transform, image.size.width, image.size.height); - transform = CGAffineTransformRotate(transform, M_PI); - break; - - case UIImageOrientationLeft: - case UIImageOrientationLeftMirrored: - transform = CGAffineTransformTranslate(transform, image.size.width, 0); - transform = CGAffineTransformRotate(transform, M_PI_2); - break; - - case UIImageOrientationRight: - case UIImageOrientationRightMirrored: - transform = CGAffineTransformTranslate(transform, 0, image.size.height); - transform = CGAffineTransformRotate(transform, -M_PI_2); - break; - case UIImageOrientationUp: - case UIImageOrientationUpMirrored: - break; - } - - switch (position) { - case AVCaptureDevicePositionFront: - switch (image.imageOrientation) { - - case UIImageOrientationLeft: - case UIImageOrientationLeftMirrored: - case UIImageOrientationRight: - case UIImageOrientationRightMirrored: - transform = CGAffineTransformTranslate(transform, 0, image.size.width); - transform = CGAffineTransformScale(transform, 1, -1); - break; - case UIImageOrientationDown: - case UIImageOrientationDownMirrored: - case UIImageOrientationUp: - case UIImageOrientationUpMirrored: - default: - transform = CGAffineTransformTranslate(transform, image.size.width, 0); - transform = CGAffineTransformScale(transform, -1, 1); - break; - } - - break; - case AVCaptureDevicePositionUnspecified: - case AVCaptureDevicePositionBack: - default: - break; - } - - CGContextRef ctx = CGBitmapContextCreate(NULL, image.size.width, image.size.height, - CGImageGetBitsPerComponent(image.CGImage), 0, - CGImageGetColorSpace(image.CGImage), - CGImageGetBitmapInfo(image.CGImage)); - CGContextConcatCTM(ctx, transform); - switch (image.imageOrientation) { - case UIImageOrientationLeft: - case UIImageOrientationLeftMirrored: - case UIImageOrientationRight: - case UIImageOrientationRightMirrored: - - CGContextDrawImage(ctx, CGRectMake(0,0,image.size.height,image.size.width), image.CGImage); - break; - - default: - CGContextDrawImage(ctx, CGRectMake(0,0,image.size.width,image.size.height), image.CGImage); - break; - } - CGImageRef cgimg = CGBitmapContextCreateImage(ctx); - UIImage *img = [UIImage imageWithCGImage:cgimg]; - CGContextRelease(ctx); - CGImageRelease(cgimg); - return img; -} -#pragma mark - AVCapture{Audio,Video}DataOutputSampleBufferDelegate +#pragma mark - Private Methods -- (void)captureOutput:(AVCaptureOutput *)captureOutput -didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer - fromConnection:(AVCaptureConnection *)connection +- (void)runMediaStreamRecordingWithError:(NSError *)error + response:(DConnectResponseMessage *)response + state:(NSString *)state { - CMSampleBufferRef buffer = sampleBuffer; - // オーディオ・ビデオのどちらからデータが来たかのフラグ - BOOL isAudio; - if ([captureOutput isKindOfClass:[AVCaptureAudioDataOutput class]]) { - isAudio = YES; - } else if ([captureOutput isKindOfClass:[AVCaptureVideoDataOutput class]]) { - isAudio = NO; - } else { - NSLog(@"Capture output \"%s\" is not supported.", object_getClassName([captureOutput class])); - return; - } - - CMTime originalSampleBufferTimestamp = CMSampleBufferGetPresentationTimeStamp(buffer); - if (!CMTIME_IS_NUMERIC(originalSampleBufferTimestamp)) { - NSLog(@"Invalid %@ timestamp; could not append the sample.", isAudio ? @"audio" : @"video"); - return; - } - - BOOL updateLastSampleTimestamp = YES; - BOOL adjustTimestamp = YES; - BOOL initMuteSample = YES; - BOOL requireRelease = NO; - CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(buffer); - for (DPHostRecorderContext *recorder in _recorderArr) { - if (recorder.state != RecorderStateRecording) { - continue; - } - - if (isAudio) { - // オーディオ - if (recorder.audioConnection != connection) { - continue; - } - if (!recorder.audioReady && - ![self setupAssetWriterAudioInputForRecorderContext:recorder - description:formatDescription]) - { - // キャプチャーセッションを停止する。 - [recorder.session stopRunning]; - [recorder.response setErrorToUnknownWithMessage: - [NSString stringWithFormat: - @"Failed to add an audio input to an asset writer for recorder \"%@\".", - recorder.name]]; - [recorder sendResponse]; - recorder.state = RecorderStateInactive; - recorder.audioReady = recorder.videoReady = NO; - recorder.writer = nil; - recorder.audioWriterInput = recorder.videoWriterInput = nil; - - continue; - } - } else { - // ビデオ - if (recorder.videoConnection != connection) { - continue; - } - if (!recorder.videoReady && - ![self setupAssetWriterVideoInputForRecorderContext:recorder - description:formatDescription]) - { - - // キャプチャーセッションを停止する。 - [recorder.session stopRunning]; - - [recorder.response setErrorToUnknownWithMessage: - [NSString stringWithFormat: - @"Failed to add an video input to an asset writer for recorder \"%@\".", - recorder.name]]; - [recorder sendResponse]; - - // TODO: レコーダーの初期化コードを関数化 - recorder.state = RecorderStateInactive; - recorder.audioReady = recorder.videoReady = NO; - recorder.writer = nil; - recorder.audioWriterInput = recorder.videoWriterInput = nil; - - continue; - } - } + if (!error) { + [response setResult:DConnectMessageResultTypeOk]; + [self sendOnRecordingChangeEventWithStatus:state + path:nil + mimeType:nil + errorMessage:nil]; - if ((!recorder.audioDevice || recorder.audioReady) && - (!recorder.videoDevice || recorder.videoReady)) { - if (_needRecalculationOfTotalPauseDuration) { - if (!isAudio) { - return; - } - - if (CMTIME_IS_NUMERIC(_lastSampleTimestamp)) { - CMTime sampleBufferTimestamp = originalSampleBufferTimestamp; - if (CMTIME_IS_NUMERIC(_totalPauseDuration)) { - sampleBufferTimestamp = CMTimeSubtract(sampleBufferTimestamp, _totalPauseDuration); - } - CMTime pauseDuration = CMTimeSubtract(sampleBufferTimestamp, _lastSampleTimestamp); - - if (CMTIME_IS_NUMERIC(_totalPauseDuration) && _totalPauseDuration.value != 0) { - _totalPauseDuration = CMTimeAdd(_totalPauseDuration, pauseDuration); - } else { - _totalPauseDuration = pauseDuration; - } - } - _lastSampleTimestamp.flags = 0; - _needRecalculationOfTotalPauseDuration = NO; - } - - if (adjustTimestamp) { - CFRetain(buffer); - - // ポーズの累計期間に応じたサンプルのタイミング修正を行う。 - if (CMTIME_IS_NUMERIC(_totalPauseDuration) && _totalPauseDuration.value != 0) { - // タイムスタンプのタイムスタンプをポーズの累計期間に応じて調整する - CMSampleBufferRef tmp = [self sampleBufferByAdjustingTimestamp:sampleBuffer by:_totalPauseDuration]; - CFRelease(sampleBuffer); - buffer = tmp; - } - adjustTimestamp = NO; - requireRelease = YES; - } - - if (isAudio && recorder.isMuted && initMuteSample) { - CMBlockBufferRef buf = CMSampleBufferGetDataBuffer(buffer); - size_t length; - size_t totalLength; - char* data; - if (CMBlockBufferGetDataPointer(buf, 0, &length, &totalLength, &data) != noErr) { - NSLog(@"Failed to set audio amplitude to 0 for muting."); - } else { - for (size_t i = 0; i < length; ++i) { - data[i] = 0; - } - } - initMuteSample = NO; - } - - if (!isAudio && _sendPreview && - // デフォルトカメラの時だけ - recorder == _recorderArr[[_defaultVideoRecorderId intValue]]) { - if (CMTIME_IS_INVALID(_lastPreviewTimestamp)) { - // まだプレビューの配送を行っていないのであれば、プレビューを配信する。 - [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; - } else if (CMTIME_IS_NUMERIC(_lastPreviewTimestamp)) { - CMTime elapsedTime = - CMTimeSubtract(_lastPreviewTimestamp, originalSampleBufferTimestamp); - if (CMTIME_COMPARE_INLINE(elapsedTime, >=, _secPerFrame)) { - // 規定時間が経過したのであれば、プレビューを配信する。 - [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; - } - } else { - _lastPreviewTimestamp = originalSampleBufferTimestamp; - } - } - - if (isAudio && updateLastSampleTimestamp) { - // サンプルのタイムスタンプを保持しておく - CMTime sampleBufferTimestamp = CMSampleBufferGetPresentationTimeStamp(buffer); - CMTime duration = CMSampleBufferGetDuration(sampleBuffer); - if (duration.value > 0) { - _lastSampleTimestamp = CMTimeAdd(sampleBufferTimestamp, duration); - } else { - // 「サンプルの開始時間(タイムスタンプ)」と「サンプルの終了時間」が同義。 - _lastSampleTimestamp = sampleBufferTimestamp; - } - updateLastSampleTimestamp = NO; - } - - [self appendSampleBuffer:sampleBuffer recorderContext:recorder isAudio:isAudio]; - } - } - if (requireRelease) { - CFRelease(buffer); - } -} -- (BOOL) appendSampleBuffer:(CMSampleBufferRef)sampleBuffer - recorderContext:(DPHostRecorderContext *)recorderCtx - isAudio:(BOOL)isAudio -{ - @synchronized(recorderCtx) { - if (!recorderCtx.writer) { - return NO; - } - - if (CMSampleBufferDataIsReady(sampleBuffer)) { - if (recorderCtx.writer.status == AVAssetWriterStatusUnknown) { - if ([recorderCtx.writer startWriting]) { - [recorderCtx.writer startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; - - // ライターの書き出し成功を確認、Record APIのHTTPレスポンスでOKを返却する。 - [recorderCtx.response setResult:DConnectMessageResultTypeOk]; - [recorderCtx sendResponse]; - - [self sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateRecording - path:nil mimeType:nil errorMessage:nil]; - } - else { - // ライターの書き出し失敗 - - // キャプチャーセッションを停止する。 - [recorderCtx.session stopRunning]; - - // Record APIのHTTPレスポンスでエラーを返却する。 - [recorderCtx.response setErrorToUnknownWithMessage: - [NSString stringWithFormat:@"Failed to start a session for an asset writer: %@", - recorderCtx.writer.error.localizedDescription]]; - [recorderCtx sendResponse]; - - // TODO: レコーダーの初期化コードを関数化 - recorderCtx.state = RecorderStateInactive; - recorderCtx.audioReady = recorderCtx.videoReady = NO; - recorderCtx.writer = nil; - recorderCtx.audioWriterInput = recorderCtx.videoWriterInput = nil; - - return NO; - } - } - - if (recorderCtx.writer.status == AVAssetWriterStatusFailed) { - recorderCtx.state = RecorderStateInactive; - recorderCtx.audioReady = recorderCtx.videoReady = NO; - recorderCtx.writer = nil; - recorderCtx.audioWriterInput = recorderCtx.videoWriterInput = nil; - - // TODO: エラーイベントを配送する - - return NO; - } - - if (recorderCtx.writer.status == AVAssetWriterStatusWriting) { - AVAssetWriterInput *writerInput = - isAudio ? recorderCtx.audioWriterInput : recorderCtx.videoWriterInput; - if (writerInput || sampleBuffer) { - if (!writerInput.readyForMoreMediaData) { - return NO; - } - if ([writerInput appendSampleBuffer:sampleBuffer]) { - return YES; - } else if (recorderCtx.writer.status == AVAssetWriterStatusFailed) { - return NO; - } - } - } - - // TODO: エラーイベントを配送する - NSLog(@"Failed to append a sample data."); - return NO; - } - - // TODO: エラーイベントを配送する - NSLog(@"Sample data is not ready."); - return NO; - } -} - -- (CMSampleBufferRef) sampleBufferByAdjustingTimestamp:(CMSampleBufferRef)sample by:(CMTime)offset -{ - CMItemCount count; - CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); - CMSampleTimingInfo* pInfo = malloc(sizeof(CMSampleTimingInfo) * count); - CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); - for (CMItemCount i = 0; i < count; ++i) - { - pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); - pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); - } - CMSampleBufferRef sout; - CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); - free(pInfo); - return sout; -} - -#pragma mark - UIDeviceOrientationDidChangeNotification -- (void)deviceOrientationDidChange -{ - UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; - // 縦か横の時だけ更新する。 - if ( UIDeviceOrientationIsPortrait(orientation) || UIDeviceOrientationIsLandscape(orientation) ) { - _referenceOrientation = orientation; - } -} - -- (void)setLightOff -{ - AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; - [captureDevice lockForConfiguration:NULL]; - if (captureDevice.torchMode == AVCaptureTorchModeOn) { - captureDevice.torchMode = AVCaptureTorchModeOff; + } else { + [response setError:error.code message:error.localizedDescription]; } - [captureDevice unlockForConfiguration]; } - @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.h index 94e7b1af..1f1aeaed 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.h @@ -11,6 +11,8 @@ #import #import +#import "DPHostMediaStreamRecordingProfile.h" +#import "DPHostDevicePlugin.h" typedef NS_ENUM(NSUInteger, RecorderDataSourceType) { RecorderDataSourceTypePhoto, ///< 写真(静止画) RecorderDataSourceTypeAudio, ///< オーディオ(動画における音声) @@ -28,6 +30,7 @@ typedef NS_ENUM(NSUInteger, RecorderState) { RecorderStateRecording, ///< 撮影状態「撮影中 ("recording")」 }; + /*! 入力デバイス管理クラス */ @@ -87,7 +90,8 @@ typedef NS_ENUM(NSUInteger, RecorderState) { @property (nonatomic) AVCaptureVideoOrientation videoOrientation; ///< 録画開始時のビデオの向き @property (nonatomic) AVAssetWriter *writer; ///< レコーディング内容の書き出しを行うオブジェクト -@property (nonatomic) DConnectResponseMessage *response; ///< AVAssetWriterの初期化および書き出し成功を確認した際に用いるHTTPレスポンス。 +/// @brief イベントマネージャ +@property DConnectEventManager *eventMgr; /*! キャプチャーセッション。 @@ -106,10 +110,9 @@ typedef NS_ENUM(NSUInteger, RecorderState) { @property (nonatomic) BOOL videoReady; ///< ビデオ入力デバイスのレコーディング準備が整っているかどうか /*! - @param[in] profile このコンテキストを所有しているDPHostMediaStreamRecordingProfile @return レコーディングコンテキスト */ -- (instancetype)initWithProfile:(DPHostMediaStreamRecordingProfile *)profile; +- (instancetype)init; /*! キャプチャーセッションに指定されたデバイスが既にInputとして追加されているかどうかをチェックする @@ -158,14 +161,10 @@ typedef NS_ENUM(NSUInteger, RecorderState) { @retval YES アセットライターのインスタンス化に成功 @retval NO アセットライターのインスタンス化に失敗 */ -- (BOOL) setupAssetWriterWithResponse:(DConnectResponseMessage *)response; +- (BOOL) setupAssetWriter; -/*! - HTTPレスポンスを返却する。 - 既にHTTPレスポンスを返却済みの場合は何もおこらない。 - */ -- (void) sendResponse; - (void) sendOnRecordingChangeEventWithStatus:(NSNotification *)notification; + @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.m index e0996a35..fac8798c 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderContext.m @@ -7,10 +7,11 @@ // http://opensource.org/licenses/mit-license.php // +#import #import "DPHostMediaStreamRecordingProfile.h" #import "DPHostService.h" #import "DPHostRecorderContext.h" - +#import "DPHostDevicePlugin.h" const char * const AudioCaptureQueueName = "org.deviceconnect.ios.host.mediastream_recording.audio_capture"; const char * const VideoCaptureQueueName = "org.deviceconnect.ios.host.mediastream_recording.video_capture"; @@ -84,11 +85,11 @@ @interface DPHostRecorderContext() @implementation DPHostRecorderContext -- (instancetype)initWithProfile:(DPHostMediaStreamRecordingProfile *)profile +- (instancetype)init { self = [super init]; if (self) { - self.profile = profile; + self.eventMgr = [DConnectEventManager sharedManagerForClass:[DPHostDevicePlugin class]]; self.isMuted = NO; self.videoOrientation = AVCaptureVideoOrientationPortrait; @@ -285,10 +286,9 @@ - (void) setRecorderDataSource:(DPHostRecorderDataSource *)dataSrc delegate:(id) } } -- (BOOL) setupAssetWriterWithResponse:(DConnectResponseMessage *)response; +- (BOOL) setupAssetWriter { // AVAssetWriterの初期化および書き出し成功を確認した際に用いるHTTPレスポンス。 - _response = response; NSString *fileName = [NSString stringWithFormat:@"%@_%@", [[NSProcessInfo processInfo] globallyUniqueString], @"movie.mp4"]; @@ -300,18 +300,10 @@ - (BOOL) setupAssetWriterWithResponse:(DConnectResponseMessage *)response; return _writer != nil; } -- (void) sendResponse -{ - if (_response) { - [[DConnectManager sharedManager] sendResponse:_response]; - _response = nil; - } -} - - (void) sendOnRecordingChangeEventWithStatus:(NSNotification *)notification { // イベントの取得 - NSArray *evts = [_profile.eventMgr eventListForServiceId:DPHostDevicePluginServiceId + NSArray *evts = [self.eventMgr eventListForServiceId:DPHostDevicePluginServiceId profile:DConnectMediaStreamRecordingProfileName attribute:DConnectMediaStreamRecordingProfileAttrOnPhoto]; // イベント送信 diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h new file mode 100644 index 00000000..f95cba01 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h @@ -0,0 +1,50 @@ +// +// DPHostRecorderManager.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + +#import +#import +#import + +@interface DPHostRecorderManager : NSObject +/*! + @brief DPHostRecorderManagerの共有インスタンスを返す。 + @return DPHostRecorderManagerの共有インスタンス。 + */ ++ (instancetype)sharedManager; + +// GET /mediastreamrecording/playstatus +- (NSArray*)playStatus; + +// POST /mediastreamrecording/takePhoto +- (void)takephotoForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler; + +// POST /mediastreamrecording/record +- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice response:(DConnectResponseMessage *)response completionHandler:(void (^)(NSError *error))completionHandler;; + +// PUT /mediastreamrecording/stop +- (void)stopForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler; + +// PUT /mediastreamrecording/pause +- (void)pauseForTarget:(NSString*)target error:(NSError**)error; + +// PUT /mediastreamrecording/resume +- (void)resumeForTarget:(NSString*)target error:(NSError**)error; + +// PUT /mediastreamrecording/mutetrack +- (void)muteTrackForTarget:(NSString*)target error:(NSError**)error; + +// PUT /mediastreamrecording/unmutetrack +- (void)unmuteTrackForTarget:(NSString*)target error:(NSError**)error; + +// PUT /mediaStreamRecording/preview +- (NSString*)startPreviewForTarget:(NSString*)target; + +// DELETE /mediaStreamRecording/preview +- (void)stopPreviewForTarget:(NSString*)target; +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m new file mode 100644 index 00000000..f5a47f24 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m @@ -0,0 +1,1385 @@ +// +// DPHostRecorderManager.m +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// +#import +#import +#import +#import "DPHostDevicePlugin.h" +#import "DPHostService.h" +#import "DPHostRecorderManager.h" +#import "DPHostRecorderContext.h" +#import "DPHostUtils.h" +#import "DPHostRecorderManager.h" + +@interface DPHostRecorderManager () + +/*! + デフォルトの静止画レコーダーのID + iOSデバイスによっては背面カメラが無かったりと差異があるので、 + ランタイム時にデフォルトのレコーダーを決定する処理を行う。 + */ +@property (nonatomic) NSNumber *defaultPhotoRecorderId; +/*! + デフォルトの動画レコーダーのID + iOSデバイスによっては背面カメラが無かったりと差異があるので、 + ランタイム時にデフォルトのレコーダーを決定する処理を行う。 + */ +@property (nonatomic) NSNumber *defaultVideoRecorderId; +/*! + デフォルトの音声レコーダーのID + */ +@property (nonatomic) NSNumber *defaultAudioRecorderId; +/*! + カレントのレコーダーのID + */ +@property (nonatomic) NSNumber *currentRecorderId; + + +/// レコーダーで使用できる静止画入力データ +@property (nonatomic) NSMutableArray *photoDataSourceArr; +/// レコーダーで使用できる動画入力データ +@property (nonatomic) NSMutableArray *audioDataSourceArr; +/// レコーダーで使用できる音声入力データ +@property (nonatomic) NSMutableArray *videoDataSourceArr; + +/// 使用できるレコーダー +@property (nonatomic) NSMutableArray *recorderArr; + +/// TODO: PHPhotoLibraryに変更する +@property ALAssetsLibrary *library; + + +/*! + @brief iOSデバイスの向き + 画面が天井や地面を向いた際は、無視して以前の向き情報を保持する。 + UIDeviceOrientationPortraitUpsideDown: + この場合、iOSデバイスを正面に見据えて、デバイスを反時計回りに180°回転し、 + Homeボタンが上方向にある状態。 + UIDeviceOrientationLandscapeLeft: + この場合、iOSデバイスを正面に見据えて、デバイスを反時計回りに90°回転し、 + Homeボタンが右方向にある状態。 + */ +@property (nonatomic) UIDeviceOrientation referenceOrientation; + +/// 前回プレビューを送った時間。 +@property (nonatomic) CMTime lastPreviewTimestamp; +/// Data Available Event APIでプレビュー画像URIの配送を行うかどうか。 +@property (nonatomic) BOOL sendPreview; +/// Data Available Event APIでプレビュー画像URIの配送を行うインターバル(秒)。 +@property (nonatomic) CMTime secPerFrame; + +/// ポーズ前最後のサンプルのタイムスタンプ +@property CMTime lastSampleTimestamp; +/// ポーズの累計期間 +@property CMTime totalPauseDuration; +/// ポーズの累計期間を再計算する必要が有るかどうか +@property BOOL needRecalculationOfTotalPauseDuration; +/** + @brief 現在のプレビュー画像の連番。 + Data Available Event APIで送るプレビュー画像は0-99までの連番を組み込んだ固定名を与えるの + で、現在0-99までのどの連番を使ったかを管理する。 + */ +@property int curPreviewImageEnumerator; +@end + + +@implementation DPHostRecorderManager +// 共有インスタンス ++ (instancetype)sharedManager +{ + static id sharedInstance; + static dispatch_once_t onceSpheroToken; + dispatch_once(&onceSpheroToken, ^{ + sharedInstance = [DPHostRecorderManager new]; + [sharedInstance initVariables]; + [sharedInstance initRecorderDataSource]; + [sharedInstance initPhotoRecorders]; + [sharedInstance initVideoRecorders]; + [sharedInstance initAudioRecorders]; + }); + return sharedInstance; +} + + +#pragma mark - Public method + +- (NSArray*)playStatus +{ + return self.recorderArr; +} + +- (void)takephotoForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler +{ + __block NSError *error = nil; + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypePhoto error:&error]; + if (error) { + completionHandler(nil, error); + return; + } + NSString *usedRecorderName = [self usedVideoContextForRecorder:recorder]; + if (usedRecorderName) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:[NSString stringWithFormat:@"Video device is currently used by %@.", + usedRecorderName]]; + completionHandler(nil, error); + return; + } + __weak DPHostRecorderManager *weakSelf = self; + if (recorder.videoConnection.supportsVideoOrientation) { + recorder.videoConnection.videoOrientation = videoOrientationFromDeviceOrientation([UIDevice currentDevice].orientation); + } + + [recorder performWriting: + ^{ + if (recorder.type != RecorderTypePhoto) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"target is not a video device; it is not capable of taking a photo."]; + completionHandler(nil, error); + return; + } + + if (![recorder.session isRunning]) { + [recorder.session startRunning]; + } + + // ライトが点いていたら消灯する。 + [weakSelf setLightOff]; + // 写真を撮影する。 + [weakSelf takePhotoInternal:recorder]; + + [weakSelf saveFileWithRecorder:recorder completionHandler:^(NSURL *assetURL, NSError *error) { + + if ([recorder.session isRunning]) { + [recorder.session stopRunning]; + } + completionHandler(assetURL, error); + }]; + }]; + return; +} + +// POST /mediastreamrecording/record +- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice response:(DConnectResponseMessage *)response completionHandler:(void (^)(NSError *))completionHandler +{ + __block NSError *error = nil; + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:&error]; + if (error) { + completionHandler(error); + return; + } + + if (recorder.state == RecorderStateRecording) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"target is already recording."]; + completionHandler(error); + return; + } + // 入力デバイスが既に他のレコーダーで使われていないかをチェックする + NSString *usedVideoRecorderName = [self usedVideoContextForRecorder:recorder]; + if (usedVideoRecorderName) { + // ビデオ入力デバイスが既に他のコンテキストで使われている。 + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message: + [NSString stringWithFormat:@"Video device is currently used by %@.", + usedVideoRecorderName]]; + completionHandler(error); + return; + } + NSString *usedAudioRecorderName = [self usedAudioContextForRecorder:recorder]; + if (usedAudioRecorderName) { + // オーディオ入力デバイスが既に他のコンテキストで使われている。 + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message: + [NSString stringWithFormat:@"Audio device is currently used by %@.", + usedAudioRecorderName]]; + completionHandler(error); + return; + } + __weak DPHostRecorderManager *weakSelf = self; + [recorder performWriting: + ^{ + [weakSelf startRecordingForRecorder:recorder error:&error]; + completionHandler(error); + }]; + return; +} + +// PUT /mediastreamrecording/stop +- (void)stopForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler +{ + NSError *error = nil; + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:&error]; + if (error) { + completionHandler(nil, error); + return; + } + if (recorder.state == RecorderStateInactive) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"target is not recording."]; + completionHandler(nil, error); + return; + } + + [self finishRecordingSampleForRecorder:recorder]; + + if (!recorder.writer) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"Writer is non exist."]; + completionHandler(nil, error); + return; + } + if (recorder.writer.status == AVAssetWriterStatusUnknown) { + error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"Unknown Failed to finishing an aseet writer"]; + completionHandler(nil, error); + return; + } + __weak DPHostRecorderManager *weakSelf = self; + [self saveMovieFileForRecorder:recorder completionHandler:^(NSURL *assetURL, NSError *error) { + weakSelf.currentRecorderId = nil; + recorder.writer = nil; + recorder.audioWriterInput = recorder.videoWriterInput = nil; + completionHandler(assetURL, error); + }]; +} + +// PUT /mediastreamrecording/pause +- (void)pauseForTarget:(NSString*)target error:(NSError**)error +{ + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + if (*error) { + return; + } + + if (recorder.state == RecorderStatePaused) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"target is already pausing."]; + return; + } + + if (recorder.state == RecorderStateRecording) { + if ([recorder.session isRunning]) { + [recorder.session stopRunning]; + if ([recorder.session isRunning]) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Failed to pause the specified recorder; failed to stop capture session."]; + return; + } + } + + recorder.state = RecorderStatePaused; + + [self setNeedRecalculationOfTotalPauseDuration: YES]; + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message: + @"The specified recorder is not recording; no need for pause."]; + } +} + +// PUT /mediastreamrecording/resume +- (void)resumeForTarget:(NSString*)target error:(NSError**)error +{ + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + if (*error) { + return; + } + if (recorder.state == RecorderStateRecording) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"target is not pausing."]; + return; + } + if (recorder.state == RecorderStatePaused) { + if (![recorder.session isRunning]) { + [recorder.session startRunning]; + if (![recorder.session isRunning]) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"Failed to resume the specified recorder; failed to start capture session."]; + return; + } + } + recorder.state = RecorderStateRecording; + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message: + @"The specified recorder is not recording; no need for pause."]; + } +} + +// PUT /mediastreamrecording/mutetrack +- (void)muteTrackForTarget:(NSString*)target error:(NSError**)error +{ + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + if (*error) { + return; + } + + if (!recorder.audioDevice) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message: + @"The specified target does not capture audio and can not be muted."]; + return; + } + + if (!recorder.isMuted) { + recorder.isMuted = YES; + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"The specified recorder is already muted."]; + } + +} + +// PUT /mediastreamrecording/unmutetrack +- (void)unmuteTrackForTarget:(NSString*)target error:(NSError**)error +{ + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + if (*error) { + return; + } + + if (!recorder.audioDevice) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message: + @"The specified target does not capture audio and can not be unmuted."]; + return; + } + + if (recorder.isMuted) { + recorder.isMuted = NO; + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"The specified recorder is not muted."]; + } +} + +// PUT /mediaStreamRecording/preview +- (NSString*)startPreviewForTarget:(NSString*)target +{ + // TODO: + return nil; +} + +// DELETE /mediaStreamRecording/preview +- (void)stopPreviewForTarget:(NSString*)target +{ + // TODO: +} + + + +#pragma mark - MediaStreamRecording Profile Init Internal +- (void)initVariables +{ + self.recorderArr = [NSMutableArray array]; + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self selector:@selector(deviceOrientationDidChange) + name:UIDeviceOrientationDidChangeNotification object:nil]; + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + self.curPreviewImageEnumerator = 0; + self.currentRecorderId = nil; + self.secPerFrame = CMTimeMake(2, 1000); + self.lastPreviewTimestamp = kCMTimeInvalid; + self.lastSampleTimestamp = kCMTimeInvalid; + self.totalPauseDuration = kCMTimeInvalid; + self.needRecalculationOfTotalPauseDuration = NO; + self.library = [ALAssetsLibrary new]; + self.photoDataSourceArr = [NSMutableArray array]; + self.audioDataSourceArr = [NSMutableArray array]; + self.videoDataSourceArr = [NSMutableArray array]; + self.sendPreview = NO; +} +- (void)initRecorderDataSource +{ + AVCaptureSession *session; + NSArray *audioDevArr = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]; + NSArray *videoDevArr = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + DPHostRecorderDataSource *recCtx; + session = [AVCaptureSession new]; + for (AVCaptureDevice *audioDev in audioDevArr) { + // 出力:音声 + recCtx = [DPHostRecorderDataSource recorderDataSourceForAudioWithAudioDevice:audioDev]; + + if (recCtx) { + recCtx.position = audioDev.position; + [self.audioDataSourceArr addObject:recCtx]; + } + } + for (AVCaptureDevice *videoDev in videoDevArr) { + recCtx = [DPHostRecorderDataSource recorderDataSourceForPhotoWithVideoDevice:videoDev]; + + if (recCtx) { + recCtx.position = videoDev.position; + [self.photoDataSourceArr addObject:recCtx]; + } + session = [AVCaptureSession new]; + recCtx = [DPHostRecorderDataSource recorderDataSourceForVideoWithVideoDevice:videoDev]; + + if (recCtx) { + recCtx.position = videoDev.position; + NSMutableArray *dimensionArr = + @[ + AVCaptureSessionPreset352x288, + AVCaptureSessionPreset640x480, + AVCaptureSessionPreset1280x720, + AVCaptureSessionPreset1920x1080 + ].mutableCopy; + for (size_t i = 0; i < dimensionArr.count; ++i) { + if (![session canSetSessionPreset:dimensionArr[i]]) { + [dimensionArr removeObjectAtIndex:i]; + } + } + NSDictionary *(^getDimension)(NSString *) = ^ NSDictionary *(NSString *preset) { + if ([preset isEqualToString:AVCaptureSessionPreset352x288]) { + return @{@"h":@352 ,@"w":@288}; + } else if ([preset isEqualToString:AVCaptureSessionPreset640x480]) { + return @{@"h":@640 ,@"w":@480}; + } else if ([preset isEqualToString:AVCaptureSessionPreset1280x720]) { + return @{@"h":@1280 ,@"w":@720}; + } else if ([preset isEqualToString:AVCaptureSessionPreset1920x1080]) { + return @{@"h":@1920 ,@"w":@1080}; + } + return nil; + }; + NSDictionary *minDim = getDimension([dimensionArr firstObject]); + NSDictionary *maxDim = getDimension([dimensionArr lastObject]); + recCtx.imageHeight = recCtx.minImageHeight = minDim[@"h"]; + recCtx.imageWidth = recCtx.minImageWidth = minDim[@"w"]; + recCtx.maxImageHeight = maxDim[@"h"]; + recCtx.maxImageWidth = maxDim[@"w"]; + + [self.videoDataSourceArr addObject:recCtx]; + } + } +} + +- (void)initPhotoRecorders +{ + unsigned long videoNormalCount = 0; + unsigned long videoBackCount = 0; + unsigned long videoFrontCount = 0; + for (DPHostRecorderDataSource *dataSrc in self.photoDataSourceArr) { + if ([dataSrc.uniqueId isEqualToString:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo].uniqueID]) { + self.defaultPhotoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; + } + + DPHostRecorderContext *recorder = [[DPHostRecorderContext alloc] init]; + recorder.type = RecorderTypePhoto; + recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"jpg"]; + recorder.state = RecorderStateInactive; + + [recorder setRecorderDataSource:dataSrc delegate:self]; + + NSMutableString *name = @"photo_".mutableCopy; + switch (dataSrc.position) { + case AVCaptureDevicePositionBack: + [name appendString:@"back_"]; + [name appendString:[NSString stringWithFormat:@"%lu", videoBackCount]]; + ++videoBackCount; + break; + case AVCaptureDevicePositionFront: + [name appendString:@"front_"]; + [name appendString:[NSString stringWithFormat:@"%lu", videoFrontCount]]; + ++videoFrontCount; + break; + case AVCaptureDevicePositionUnspecified: + default: + [name appendString:[NSString stringWithFormat:@"%lu", videoNormalCount]]; + ++videoNormalCount; + break; + } + recorder.name = [NSString stringWithString:name]; + + [self.recorderArr addObject:recorder]; + } +} + +- (void)initVideoRecorders +{ + unsigned long audioVideoNormalCount = 0; + unsigned long audioVideoBackCount = 0; + unsigned long audioVideoFrontCount = 0; + for (DPHostRecorderDataSource *videoDataSrc in self.videoDataSourceArr) { + // 動画(ビデオのみ) + if (self.audioDataSourceArr.count == 0 + && [videoDataSrc.uniqueId isEqualToString:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo].uniqueID]) { + self.defaultVideoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; + } + DPHostRecorderContext *recorder; + NSMutableString *name; + + for (DPHostRecorderDataSource *audioDataSrc in self.audioDataSourceArr) { + // 動画(動画・音声) + if ([videoDataSrc.uniqueId isEqualToString:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo].uniqueID]) { + self.defaultVideoRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count]; + } + + recorder = [[DPHostRecorderContext alloc] init]; + recorder.type = RecorderTypeMovie; + recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"mp4"]; + recorder.state = RecorderStateInactive; + + [recorder setRecorderDataSource:audioDataSrc delegate:self]; + [recorder setRecorderDataSource:videoDataSrc delegate:self]; + + name = @"movie_audio_video_".mutableCopy; + switch (videoDataSrc.position) { + case AVCaptureDevicePositionBack: + [name appendString:@"back_"]; + [name appendString:[NSString stringWithFormat:@"%lu", audioVideoBackCount]]; + ++audioVideoBackCount; + break; + case AVCaptureDevicePositionFront: + [name appendString:@"front_"]; + [name appendString:[NSString stringWithFormat:@"%lu", audioVideoFrontCount]]; + ++audioVideoFrontCount; + break; + case AVCaptureDevicePositionUnspecified: + default: + [name appendString:[NSString stringWithFormat:@"%lu", audioVideoNormalCount]]; + ++audioVideoNormalCount; + break; + } + recorder.name = [NSString stringWithString:name]; + + [self.recorderArr addObject:recorder]; + } + } +} + +- (void)initAudioRecorders +{ + unsigned long audioCount = 0; + for (DPHostRecorderDataSource *audioDataSrc in self.audioDataSourceArr) { + DPHostRecorderContext *recorder; + NSMutableString *name; + + // 動画(音声のみ) + recorder = [[DPHostRecorderContext alloc] init]; + recorder.type = RecorderTypeMovie; + recorder.mimeType = [DConnectFileManager searchMimeTypeForExtension:@"mp4"]; + recorder.state = RecorderStateInactive; + + [recorder setRecorderDataSource:audioDataSrc delegate:self]; + + name = @"movie_audio_".mutableCopy; + [name appendString:[NSString stringWithFormat:@"%lu", audioCount]]; + ++audioCount; + recorder.name = [NSString stringWithString:name]; + + [self.recorderArr addObject:recorder]; + } + self.defaultAudioRecorderId = [NSNumber numberWithUnsignedInteger:self.recorderArr.count - 1]; +} + + +#pragma mark - MediaStreamRecordingProfile Common Internal +- (DPHostRecorderContext*)recorderForTarget:(NSString *)target recorderType:(RecorderType)recorderType error:(NSError**)error { + unsigned long long idx; + if (target || (target && target.length > 0)) { + BOOL success = [[NSScanner scannerWithString:target] scanUnsignedLongLong:&idx]; + if (!success) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message:@"target is invalid."]; + return nil; + } + } else if (self.defaultPhotoRecorderId && recorderType == RecorderTypePhoto) { // target省略時はデフォルトのレコーダーを指定する。 + idx = [self.defaultPhotoRecorderId unsignedLongLongValue]; + } else if (self.currentRecorderId && recorderType == RecorderTypeMovie) { + idx = [self.currentRecorderId unsignedLongLongValue]; + } else if (self.defaultVideoRecorderId && recorderType == RecorderTypeMovie) { + idx = [self.defaultVideoRecorderId unsignedLongLongValue]; + } else if (self.defaultAudioRecorderId && recorderType == RecorderTypeMovie) { + idx = [self.defaultAudioRecorderId unsignedLongLongValue]; + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message: + @"target was not specified, and no default target was set; please specify an existing target."]; + return nil; + } + unsigned long long count = (unsigned)self.recorderArr.count; + + if (!_recorderArr || count < idx) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeInvalidRequestParameter message: + @"target was not specified, and no default target was set; please specify an existing target."]; + return nil; + } + + if (recorderType == RecorderTypeMovie) { + self.currentRecorderId = [NSNumber numberWithUnsignedLongLong:idx]; + } + return self.recorderArr[(NSUInteger)idx]; +} + +- (void)setLightOff +{ + AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + [captureDevice lockForConfiguration:NULL]; + if (captureDevice.torchMode == AVCaptureTorchModeOn) { + captureDevice.torchMode = AVCaptureTorchModeOff; + } + [captureDevice unlockForConfiguration]; +} + +- (NSString *)usedVideoContextForRecorder:(DPHostRecorderContext *)recorder +{ + for (DPHostRecorderContext *recorderItr in self.recorderArr) { + if (recorderItr == recorder) { + continue; + } + if ((recorderItr.state == RecorderStateRecording) + && recorder.videoDevice && recorderItr.videoDevice && + [recorder.videoDevice.uniqueId isEqualToString:recorderItr.videoDevice.uniqueId]) { + return recorderItr.name; //使用中 + } + } + return nil; //使用されていない +} + + +#pragma mark - TakePhoto Internal +- (void)takePhotoInternal:(DPHostRecorderContext *)recorder +{ + __block AVCaptureDevice *captureDevice = [AVCaptureDevice deviceWithUniqueID:recorder.videoDevice.uniqueId]; + NSError *error; + [captureDevice lockForConfiguration:&error]; + + if (error) { + NSLog(@"Failed to acquire a configuration lock for %@.", captureDevice.uniqueID); + } else { + + if (captureDevice.focusMode != AVCaptureFocusModeContinuousAutoFocus && + [captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + captureDevice.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } else if (captureDevice.focusMode != AVCaptureFocusModeAutoFocus && + [captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + captureDevice.focusMode = AVCaptureFocusModeAutoFocus; + } else if (captureDevice.focusMode != AVCaptureFocusModeLocked && + [captureDevice isFocusModeSupported:AVCaptureFocusModeLocked]) { + captureDevice.focusMode = AVCaptureFocusModeLocked; + } + if (captureDevice.exposureMode != AVCaptureExposureModeContinuousAutoExposure && + [captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + captureDevice.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } else if (captureDevice.exposureMode != AVCaptureExposureModeAutoExpose && + [captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose]) { + captureDevice.exposureMode = AVCaptureExposureModeAutoExpose; + } else if (captureDevice.exposureMode != AVCaptureExposureModeLocked && + [captureDevice isExposureModeSupported:AVCaptureExposureModeLocked]) { + captureDevice.exposureMode = AVCaptureExposureModeLocked; + } + if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance && + [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) { + captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance; + } else if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeAutoWhiteBalance && + [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) { + captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeAutoWhiteBalance; + } else if (captureDevice.whiteBalanceMode != AVCaptureWhiteBalanceModeLocked && + [captureDevice isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) { + captureDevice.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked; + } + if (captureDevice.automaticallyEnablesLowLightBoostWhenAvailable != NO && + captureDevice.lowLightBoostSupported) { + captureDevice.automaticallyEnablesLowLightBoostWhenAvailable = YES; + } + [captureDevice unlockForConfiguration]; + + [NSThread sleepForTimeInterval:0.5]; + } +} + + +- (void)saveFileWithRecorder:(DPHostRecorderContext *)recorder completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler +{ + AVCaptureStillImageOutput *stillImageOutput = (AVCaptureStillImageOutput *)recorder.videoConnection.output; + __weak DPHostRecorderManager *weakSelf = self; + [stillImageOutput captureStillImageAsynchronouslyFromConnection:recorder.videoConnection + completionHandler: + ^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { + __block NSError *err = nil; + if (!imageDataSampleBuffer || error) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Failed to take a photo."]; + completionHandler(nil, err); + return; + } + NSData *jpegData; + @try { + jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; + } + @catch (NSException *exception) { + NSString *message; + if ([[exception name] isEqualToString:NSInvalidArgumentException]) { + message = @"Non-JPEG data was given."; + } else { + message = [NSString stringWithFormat:@"%@ encountered.", [exception name]]; + } + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:message]; + completionHandler(nil, err); + return; + } + + // EXIF情報を水平に統一する。ブラウザによってはEXIF情報により画像の向きが変わるため。 + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)jpegData, NULL); + NSDictionary *metadata = (__bridge NSDictionary*) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL); + NSMutableDictionary *meta = [NSMutableDictionary dictionaryWithDictionary:metadata]; + NSMutableDictionary *tiff = meta[(NSString*) kCGImagePropertyTIFFDictionary]; + tiff[(NSString*) kCGImagePropertyTIFFOrientation] = @(kCGImagePropertyOrientationUp); + meta[(NSString*) kCGImagePropertyTIFFDictionary] = tiff; + meta[(NSString*) kCGImagePropertyOrientation] = @(kCGImagePropertyOrientationUp); + UIImage *jpeg = [[UIImage alloc] initWithData:jpegData]; + UIImage *fixJpeg = [weakSelf fixOrientationWithImage:jpeg position:recorder.videoDevice.position]; + [[weakSelf library] writeImageToSavedPhotosAlbum:fixJpeg.CGImage metadata:meta completionBlock: + ^(NSURL *assetURL, NSError *error) { + if (!assetURL || error) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Failed to save a photo to camera roll."]; + completionHandler(nil, err); + return; + } + completionHandler(assetURL, err); + }]; + }]; +} +- (UIImage *)fixOrientationWithImage:(UIImage *)image position:(AVCaptureDevicePosition) position{ + + if (image.imageOrientation == UIImageOrientationUp && position != AVCaptureDevicePositionFront) return image; + + CGAffineTransform transform = CGAffineTransformIdentity; + + switch (image.imageOrientation) { + case UIImageOrientationDown: + case UIImageOrientationDownMirrored: + transform = CGAffineTransformTranslate(transform, image.size.width, image.size.height); + transform = CGAffineTransformRotate(transform, M_PI); + break; + + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + transform = CGAffineTransformTranslate(transform, image.size.width, 0); + transform = CGAffineTransformRotate(transform, M_PI_2); + break; + + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, 0, image.size.height); + transform = CGAffineTransformRotate(transform, -M_PI_2); + break; + case UIImageOrientationUp: + case UIImageOrientationUpMirrored: + break; + } + + switch (position) { + case AVCaptureDevicePositionFront: + switch (image.imageOrientation) { + + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + transform = CGAffineTransformTranslate(transform, 0, image.size.width); + transform = CGAffineTransformScale(transform, 1, -1); + break; + case UIImageOrientationDown: + case UIImageOrientationDownMirrored: + case UIImageOrientationUp: + case UIImageOrientationUpMirrored: + default: + transform = CGAffineTransformTranslate(transform, image.size.width, 0); + transform = CGAffineTransformScale(transform, -1, 1); + break; + } + + break; + case AVCaptureDevicePositionUnspecified: + case AVCaptureDevicePositionBack: + default: + break; + } + + CGContextRef ctx = CGBitmapContextCreate(NULL, image.size.width, image.size.height, + CGImageGetBitsPerComponent(image.CGImage), 0, + CGImageGetColorSpace(image.CGImage), + CGImageGetBitmapInfo(image.CGImage)); + CGContextConcatCTM(ctx, transform); + switch (image.imageOrientation) { + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + + CGContextDrawImage(ctx, CGRectMake(0,0,image.size.height,image.size.width), image.CGImage); + break; + + default: + CGContextDrawImage(ctx, CGRectMake(0,0,image.size.width,image.size.height), image.CGImage); + break; + } + CGImageRef cgimg = CGBitmapContextCreateImage(ctx); + UIImage *img = [UIImage imageWithCGImage:cgimg]; + CGContextRelease(ctx); + CGImageRelease(cgimg); + return img; +} + +#pragma mark - Record Internal +AVCaptureVideoOrientation videoOrientationFromDeviceOrientation(UIDeviceOrientation deviceOrientation) +{ + AVCaptureVideoOrientation orientation; + switch (deviceOrientation) { + case UIDeviceOrientationUnknown: + orientation = AVCaptureVideoOrientationPortrait; + break; + case UIDeviceOrientationPortrait: + orientation = AVCaptureVideoOrientationPortrait; + break; + case UIDeviceOrientationPortraitUpsideDown: + orientation = AVCaptureVideoOrientationPortraitUpsideDown; + break; + case UIDeviceOrientationLandscapeLeft: + orientation = AVCaptureVideoOrientationLandscapeRight; + break; + case UIDeviceOrientationLandscapeRight: + orientation = AVCaptureVideoOrientationLandscapeLeft; + break; + case UIDeviceOrientationFaceUp: + orientation = AVCaptureVideoOrientationPortrait; + break; + case UIDeviceOrientationFaceDown: + orientation = AVCaptureVideoOrientationPortrait; + break; + } + return orientation; +} +- (NSString *)usedAudioContextForRecorder:(DPHostRecorderContext *)recorder +{ + for (DPHostRecorderContext *recorderItr in self.recorderArr) { + if (recorderItr == recorder) { + continue; + } + if (recorderItr.state == RecorderStateRecording) { + if (recorder.audioDevice && recorderItr.audioDevice && + [recorder.audioDevice.uniqueId isEqualToString:recorderItr.audioDevice.uniqueId]) { + return recorderItr.name; // 使用中 + } + } + } + return nil; //使用されていない +} + +- (void)adjustExposureAndFocus:(AVCaptureDevice *)captureDevice { + CGPoint pointOfInterest = CGPointMake(.5, .5); + if ([captureDevice isFocusPointOfInterestSupported] && + [captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + captureDevice.focusPointOfInterest = pointOfInterest; + captureDevice.focusMode = AVCaptureFocusModeAutoFocus; + } + if ([captureDevice isExposurePointOfInterestSupported] && + [captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + captureDevice.exposurePointOfInterest = pointOfInterest; + captureDevice.exposureMode = + AVCaptureExposureModeContinuousAutoExposure; + } +} + +- (void)startRecordingForRecorder:(DPHostRecorderContext *)recorder error:(NSError **)error { + if (recorder.type != RecorderTypeMovie) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message: + @"target is not an audiovisual device; it is not capable of taking a movie."]; + return; + } + + // ライトが点いていたら消灯する。 + [self setLightOff]; + + recorder.videoOrientation = [recorder.videoConnection videoOrientation]; + + AVCaptureDevice *captureDevice = [AVCaptureDevice deviceWithUniqueID:recorder.videoDevice.uniqueId]; + NSError *err = nil; + [captureDevice lockForConfiguration:&err]; + if (err) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:[NSString stringWithFormat:@"Failed to acquire a configuration lock for %@.", captureDevice.uniqueID]]; + return; + } else { + + // 画面中央に露光やフォーカスが調整される様にする。 + [self adjustExposureAndFocus:captureDevice]; + [captureDevice unlockForConfiguration]; + + // 露光の為に少し待つ + [NSThread sleepForTimeInterval:0.5]; + } + + [recorder setupAssetWriter]; + recorder.state = RecorderStateRecording; + + // ポーズ関連の変数を初期化 + self.lastPreviewTimestamp = kCMTimeInvalid; + self.totalPauseDuration = kCMTimeInvalid; + self.needRecalculationOfTotalPauseDuration = NO; + + if (![recorder.session isRunning]) { + [recorder.session startRunning]; + } +} +#pragma mark - Stop Internal +- (void)finishRecordingSampleForRecorder:(DPHostRecorderContext *)recorder { + [recorder performWriting: + ^{ + // レコーディングサンプルの配信を停止する。 + [recorder.session stopRunning]; + + if (recorder.audioWriterInput) { + if (recorder.writer.status != AVAssetWriterStatusUnknown) { + [recorder.audioWriterInput markAsFinished]; + } + } + if (recorder.videoWriterInput) { + if (recorder.writer.status != AVAssetWriterStatusUnknown) { + [recorder.videoWriterInput markAsFinished]; + } + } + + recorder.state = RecorderStateInactive; + recorder.audioReady = recorder.videoReady = NO; + }]; +} + +- (void)saveMovieFileForRecorder:(DPHostRecorderContext *)recorder completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler { + __block NSError *err = nil; + __weak DPHostRecorderManager *weakSelf = self; + [recorder.writer finishWritingWithCompletionHandler: + ^{ + + if (recorder.writer.status == AVAssetWriterStatusFailed) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Failed to finishing an aseet writer"]; + completionHandler(nil, err); + return; + } + NSURL *fileUrl = recorder.writer.outputURL; + + // 動画をカメラロールに追加。 + [weakSelf.library writeVideoAtPathToSavedPhotosAlbum:fileUrl + completionBlock: + ^(NSURL *assetURL, NSError *error) { + if (error) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:[NSString stringWithFormat:@"Failed to save a movie to camera roll:%@.", error.localizedDescription]]; + completionHandler(nil, err); + return; + } else if (!assetURL) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Failed to save a movie to camera roll; aseetURL is nil."]; + completionHandler(nil, err); + return; + } + NSFileManager *fileMgr = [NSFileManager defaultManager]; + if ([fileMgr fileExistsAtPath:[fileUrl path]] + && ![fileMgr removeItemAtURL:fileUrl error:&err]) { + if (!err) { + err = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeUnknown message:@"Failed to remove a movie file."]; + } + completionHandler(nil, err); + return; + } + completionHandler(assetURL, nil); + }]; + }]; +} +#pragma mark - OnDataAvailable(old) + +- (BOOL) setupAssetWriterAudioInputForRecorderContext:(DPHostRecorderContext *)recorderCtx + description:(CMFormatDescriptionRef)currentFormatDescription +{ + if (!recorderCtx.writer) { + NSLog(@"assetWriter must be specified."); + return NO; + } + + const AudioStreamBasicDescription *currentASBD + = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription); + + size_t aclSize = 0; + const AudioChannelLayout *currentChannelLayout + = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize); + NSData *currentChannelLayoutData = nil; + + if ( currentChannelLayout && aclSize > 0 ) { + currentChannelLayoutData = [NSData dataWithBytes:currentChannelLayout length:aclSize]; + } else { + currentChannelLayoutData = [NSData data]; + } + NSDictionary *audioCompressionSettings = + @{ + AVFormatIDKey : @(kAudioFormatMPEG4AAC), + AVSampleRateKey : @(currentASBD->mSampleRate), + AVEncoderBitRatePerChannelKey : @64000, + AVNumberOfChannelsKey : @(currentASBD->mChannelsPerFrame), + AVChannelLayoutKey : currentChannelLayoutData + }; + if ([recorderCtx.writer canApplyOutputSettings:audioCompressionSettings forMediaType:AVMediaTypeAudio]) { + AVAssetWriterInput *assetWriterAudioIn = recorderCtx.audioWriterInput + = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings]; + assetWriterAudioIn.expectsMediaDataInRealTime = YES; + + if ([recorderCtx.writer canAddInput:assetWriterAudioIn]) { + [recorderCtx.writer addInput:assetWriterAudioIn]; + recorderCtx.audioReady = YES; + } + else { + NSLog(@"Could not add asset writer audio input."); + return NO; + } + } + else { + NSLog(@"Could not apply audio output settings."); + return NO; + } + + return YES; +} +- (BOOL) setupAssetWriterVideoInputForRecorderContext:(DPHostRecorderContext *)recorderCtx + description:(CMFormatDescriptionRef)currentFormatDescription +{ + if (!recorderCtx.writer) { + NSLog(@"assetWriter must be specified."); + return NO; + } + float bitsPerPixel; + CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription); + int numPixels = dimensions.width * dimensions.height; + int bitsPerSecond; + + // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate + if ( numPixels < (640 * 480) ) + bitsPerPixel = 4.05; // This bitrate matches the quality produced by AVCaptureSessionPresetMedium or Low. + else + bitsPerPixel = 11.4; // This bitrate matches the quality produced by AVCaptureSessionPresetHigh. + + bitsPerSecond = numPixels * bitsPerPixel; + + NSDictionary *videoCompressionSettings = + @{ + AVVideoCodecKey : AVVideoCodecH264, + AVVideoWidthKey : @(dimensions.width), + AVVideoHeightKey : @(dimensions.height), + AVVideoCompressionPropertiesKey : @{ + AVVideoAverageBitRateKey : @(bitsPerSecond), + AVVideoMaxKeyFrameIntervalKey : @30, + }, + }; + if ([recorderCtx.writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo]) { + AVAssetWriterInput *assetWriterVideoIn = recorderCtx.videoWriterInput + = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings]; + assetWriterVideoIn.expectsMediaDataInRealTime = YES; + + assetWriterVideoIn.transform = + [self transformVideoOrientation:recorderCtx.videoOrientation position:recorderCtx.videoDevice.position]; + if ([recorderCtx.writer canAddInput:assetWriterVideoIn]) { + [recorderCtx.writer addInput:assetWriterVideoIn]; + recorderCtx.videoReady = YES; + } + else { + NSLog(@"Couldn't add asset writer video input."); + return NO; + } + } + else { + NSLog(@"Couldn't apply video output settings."); + return NO; + } + + return YES; +} + +- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(UIDeviceOrientation)orientation + position:(AVCaptureDevicePosition)position +{ + CGFloat angle = 0.0; + + switch (orientation) { + case UIDeviceOrientationPortrait: + angle = 0.0; + break; + case UIDeviceOrientationPortraitUpsideDown: + angle = M_PI; + break; + case UIDeviceOrientationLandscapeLeft: + angle = position == AVCaptureDevicePositionBack ? -M_PI_2 : M_PI_2; + break; + case UIDeviceOrientationLandscapeRight: + angle = position == AVCaptureDevicePositionBack ? M_PI_2 : -M_PI_2; + break; + default: + break; + } + + return angle; +} + +- (CGAffineTransform)transformVideoOrientation:(AVCaptureVideoOrientation)orientation + position:(AVCaptureDevicePosition)position +{ + CGAffineTransform transform = CGAffineTransformIdentity; + + // iOSデバイスの向きが、ポートレート状態から角度的に何度の差があるか算出。 + CGFloat orientationAngleOffset = + [self angleOffsetFromPortraitOrientationToOrientation:_referenceOrientation + position:position]; + CGFloat videoOrientationAngleOffset = + [self angleOffsetFromPortraitOrientationToOrientation:(UIDeviceOrientation)orientation + position:position]; + + // Find the difference in angle between the passed in orientation and the current video orientation + CGFloat angleOffset = orientationAngleOffset - videoOrientationAngleOffset; + transform = CGAffineTransformMakeRotation(angleOffset); + + return transform; +} +#pragma mark - AVCapture{Audio,Video}DataOutputSampleBufferDelegate + +- (void)captureOutput:(AVCaptureOutput *)captureOutput +didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection +{ + CMSampleBufferRef buffer = sampleBuffer; + // オーディオ・ビデオのどちらからデータが来たかのフラグ + BOOL isAudio; + if ([captureOutput isKindOfClass:[AVCaptureAudioDataOutput class]]) { + isAudio = YES; + } else if ([captureOutput isKindOfClass:[AVCaptureVideoDataOutput class]]) { + isAudio = NO; + } else { + NSLog(@"Capture output \"%s\" is not supported.", object_getClassName([captureOutput class])); + return; + } + + CMTime originalSampleBufferTimestamp = CMSampleBufferGetPresentationTimeStamp(buffer); + if (!CMTIME_IS_NUMERIC(originalSampleBufferTimestamp)) { + NSLog(@"Invalid %@ timestamp; could not append the sample.", isAudio ? @"audio" : @"video"); + return; + } + + BOOL updateLastSampleTimestamp = YES; + BOOL adjustTimestamp = YES; + BOOL initMuteSample = YES; + BOOL requireRelease = NO; + CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(buffer); + for (DPHostRecorderContext *recorder in _recorderArr) { + if (recorder.state != RecorderStateRecording) { + continue; + } + + if (isAudio) { + // オーディオ + if (recorder.audioConnection != connection) { + continue; + } + if (!recorder.audioReady && + ![self setupAssetWriterAudioInputForRecorderContext:recorder + description:formatDescription]) + { + // キャプチャーセッションを停止する。 + [recorder.session stopRunning]; + recorder.state = RecorderStateInactive; + recorder.audioReady = recorder.videoReady = NO; + recorder.writer = nil; + recorder.audioWriterInput = recorder.videoWriterInput = nil; + + continue; + } + } else { + // ビデオ + if (recorder.videoConnection != connection) { + continue; + } + if (!recorder.videoReady && + ![self setupAssetWriterVideoInputForRecorderContext:recorder + description:formatDescription]) + { + + // キャプチャーセッションを停止する。 + [recorder.session stopRunning]; + + + // TODO: レコーダーの初期化コードを関数化 + recorder.state = RecorderStateInactive; + recorder.audioReady = recorder.videoReady = NO; + recorder.writer = nil; + recorder.audioWriterInput = recorder.videoWriterInput = nil; + + continue; + } + } + + if ((!recorder.audioDevice || recorder.audioReady) && + (!recorder.videoDevice || recorder.videoReady)) { + if (_needRecalculationOfTotalPauseDuration) { + if (!isAudio) { + return; + } + + if (CMTIME_IS_NUMERIC(_lastSampleTimestamp)) { + CMTime sampleBufferTimestamp = originalSampleBufferTimestamp; + if (CMTIME_IS_NUMERIC(_totalPauseDuration)) { + sampleBufferTimestamp = CMTimeSubtract(sampleBufferTimestamp, _totalPauseDuration); + } + CMTime pauseDuration = CMTimeSubtract(sampleBufferTimestamp, _lastSampleTimestamp); + + if (CMTIME_IS_NUMERIC(_totalPauseDuration) && _totalPauseDuration.value != 0) { + _totalPauseDuration = CMTimeAdd(_totalPauseDuration, pauseDuration); + } else { + _totalPauseDuration = pauseDuration; + } + } + _lastSampleTimestamp.flags = 0; + _needRecalculationOfTotalPauseDuration = NO; + } + + if (adjustTimestamp) { + CFRetain(buffer); + + // ポーズの累計期間に応じたサンプルのタイミング修正を行う。 + if (CMTIME_IS_NUMERIC(_totalPauseDuration) && _totalPauseDuration.value != 0) { + // タイムスタンプのタイムスタンプをポーズの累計期間に応じて調整する + CMSampleBufferRef tmp = [self sampleBufferByAdjustingTimestamp:sampleBuffer by:_totalPauseDuration]; + CFRelease(sampleBuffer); + buffer = tmp; + } + adjustTimestamp = NO; + requireRelease = YES; + } + + if (isAudio && recorder.isMuted && initMuteSample) { + CMBlockBufferRef buf = CMSampleBufferGetDataBuffer(buffer); + size_t length; + size_t totalLength; + char* data; + if (CMBlockBufferGetDataPointer(buf, 0, &length, &totalLength, &data) != noErr) { + NSLog(@"Failed to set audio amplitude to 0 for muting."); + } else { + for (size_t i = 0; i < length; ++i) { + data[i] = 0; + } + } + initMuteSample = NO; + } + + if (!isAudio && _sendPreview && + // デフォルトカメラの時だけ + recorder == _recorderArr[[_defaultVideoRecorderId intValue]]) { + if (CMTIME_IS_INVALID(_lastPreviewTimestamp)) { + // まだプレビューの配送を行っていないのであれば、プレビューを配信する。 +// [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; + } else if (CMTIME_IS_NUMERIC(_lastPreviewTimestamp)) { + CMTime elapsedTime = + CMTimeSubtract(_lastPreviewTimestamp, originalSampleBufferTimestamp); + if (CMTIME_COMPARE_INLINE(elapsedTime, >=, _secPerFrame)) { + // 規定時間が経過したのであれば、プレビューを配信する。 +// [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; + } + } else { + _lastPreviewTimestamp = originalSampleBufferTimestamp; + } + } + + if (isAudio && updateLastSampleTimestamp) { + // サンプルのタイムスタンプを保持しておく + CMTime sampleBufferTimestamp = CMSampleBufferGetPresentationTimeStamp(buffer); + CMTime duration = CMSampleBufferGetDuration(sampleBuffer); + if (duration.value > 0) { + _lastSampleTimestamp = CMTimeAdd(sampleBufferTimestamp, duration); + } else { + // 「サンプルの開始時間(タイムスタンプ)」と「サンプルの終了時間」が同義。 + _lastSampleTimestamp = sampleBufferTimestamp; + } + updateLastSampleTimestamp = NO; + } + + [self appendSampleBuffer:sampleBuffer recorderContext:recorder isAudio:isAudio]; + } + } + if (requireRelease) { + CFRelease(buffer); + } +} + +- (BOOL) appendSampleBuffer:(CMSampleBufferRef)sampleBuffer + recorderContext:(DPHostRecorderContext *)recorderCtx + isAudio:(BOOL)isAudio +{ + @synchronized(recorderCtx) { + if (!recorderCtx.writer) { + return NO; + } + + if (CMSampleBufferDataIsReady(sampleBuffer)) { + if (recorderCtx.writer.status == AVAssetWriterStatusUnknown) { + if ([recorderCtx.writer startWriting]) { + [recorderCtx.writer startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)]; + } + else { + // ライターの書き出し失敗 + + // キャプチャーセッションを停止する。 + [recorderCtx.session stopRunning]; + + + // TODO: レコーダーの初期化コードを関数化 + recorderCtx.state = RecorderStateInactive; + recorderCtx.audioReady = recorderCtx.videoReady = NO; + recorderCtx.writer = nil; + recorderCtx.audioWriterInput = recorderCtx.videoWriterInput = nil; + + return NO; + } + } + + if (recorderCtx.writer.status == AVAssetWriterStatusFailed) { + recorderCtx.state = RecorderStateInactive; + recorderCtx.audioReady = recorderCtx.videoReady = NO; + recorderCtx.writer = nil; + recorderCtx.audioWriterInput = recorderCtx.videoWriterInput = nil; + + // TODO: エラーイベントを配送する + + return NO; + } + + if (recorderCtx.writer.status == AVAssetWriterStatusWriting) { + AVAssetWriterInput *writerInput = + isAudio ? recorderCtx.audioWriterInput : recorderCtx.videoWriterInput; + if (writerInput || sampleBuffer) { + if (!writerInput.readyForMoreMediaData) { + return NO; + } + if ([writerInput appendSampleBuffer:sampleBuffer]) { + return YES; + } else if (recorderCtx.writer.status == AVAssetWriterStatusFailed) { + return NO; + } + } + } + + // TODO: エラーイベントを配送する + NSLog(@"Failed to append a sample data."); + return NO; + } + + // TODO: エラーイベントを配送する + NSLog(@"Sample data is not ready."); + return NO; + } +} + +- (CMSampleBufferRef) sampleBufferByAdjustingTimestamp:(CMSampleBufferRef)sample by:(CMTime)offset +{ + CMItemCount count; + CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); + CMSampleTimingInfo* pInfo = malloc(sizeof(CMSampleTimingInfo) * count); + CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); + for (CMItemCount i = 0; i < count; ++i) + { + pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); + pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); + } + CMSampleBufferRef sout; + CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); + free(pInfo); + return sout; +} + +#pragma mark - UIDeviceOrientationDidChangeNotification +- (void)deviceOrientationDidChange +{ + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + // 縦か横の時だけ更新する。 + if ( UIDeviceOrientationIsPortrait(orientation) || UIDeviceOrientationIsLandscape(orientation) ) { + _referenceOrientation = orientation; + } +} +@end From d37153bc3f821c020c21fe85bb501fbda2756b2d Mon Sep 17 00:00:00 2001 From: TakayukiHoshi1984 Date: Mon, 16 Oct 2017 16:00:43 +0900 Subject: [PATCH 03/13] =?UTF-8?q?Preview=20API=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 8 + .../DPHostMediaStreamRecordingProfile.m | 146 ++-------- .../DPHostRecorderManager.h | 6 +- .../DPHostRecorderManager.m | 171 +++++++++-- .../DPHostSimpleHttpServer.h | 56 ++++ .../DPHostSimpleHttpServer.m | 272 ++++++++++++++++++ 6 files changed, 511 insertions(+), 148 deletions(-) create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.h create mode 100644 dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.m diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj index e8ba0fef..9775c634 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ ABA43AC41EE562ED0019FE35 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABA43AC31EE562ED0019FE35 /* UserNotifications.framework */; }; ABDA3C6D1A0C58F3001F30A0 /* DPHostConnectionProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */; }; ABDA3C701A0C743A001F30A0 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABDA3C6F1A0C743A001F30A0 /* CoreBluetooth.framework */; }; + ABDC5BDD1F945AEE007201D9 /* DPHostSimpleHttpServer.m in Sources */ = {isa = PBXBuildFile; fileRef = ABDC5BDC1F945AEE007201D9 /* DPHostSimpleHttpServer.m */; }; ABED44AB1F92396B00598460 /* DPHostRecorderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = ABED44AA1F92396B00598460 /* DPHostRecorderManager.m */; }; D62F0D1A1AA6E2CB00BC0ADA /* close_small_button_focus.png in Resources */ = {isa = PBXBuildFile; fileRef = D62F0D171AA6E2CB00BC0ADA /* close_small_button_focus.png */; }; D62F0D1B1AA6E2CB00BC0ADA /* close_small_button_normal.png in Resources */ = {isa = PBXBuildFile; fileRef = D62F0D181AA6E2CB00BC0ADA /* close_small_button_normal.png */; }; @@ -168,6 +169,8 @@ ABDA3C6B1A0C58F3001F30A0 /* DPHostConnectionProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DPHostConnectionProfile.h; sourceTree = ""; }; ABDA3C6C1A0C58F3001F30A0 /* DPHostConnectionProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DPHostConnectionProfile.m; sourceTree = ""; }; ABDA3C6F1A0C743A001F30A0 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; }; + ABDC5BDB1F945AED007201D9 /* DPHostSimpleHttpServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostSimpleHttpServer.h; sourceTree = ""; }; + ABDC5BDC1F945AEE007201D9 /* DPHostSimpleHttpServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostSimpleHttpServer.m; sourceTree = ""; }; ABED44A91F92396B00598460 /* DPHostRecorderManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DPHostRecorderManager.h; sourceTree = ""; }; ABED44AA1F92396B00598460 /* DPHostRecorderManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DPHostRecorderManager.m; sourceTree = ""; }; D62F0D171AA6E2CB00BC0ADA /* close_small_button_focus.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = close_small_button_focus.png; path = dConnectDeviceHost_resources/close_small_button_focus.png; sourceTree = SOURCE_ROOT; }; @@ -245,6 +248,8 @@ 25B42C6219865EC400B65E83 /* DPHostMediaStreamRecording */ = { isa = PBXGroup; children = ( + ABDC5BDB1F945AED007201D9 /* DPHostSimpleHttpServer.h */, + ABDC5BDC1F945AEE007201D9 /* DPHostSimpleHttpServer.m */, 25C710E91981E578004C6484 /* DPHostMediaStreamRecordingProfile.h */, 25C710EA1981E578004C6484 /* DPHostMediaStreamRecordingProfile.m */, 25B42C6319865F1000B65E83 /* DPHostRecorderContext.h */, @@ -668,6 +673,7 @@ D6384EA91A7625BD00384167 /* DPHostCanvasProfile.m in Sources */, AB912A7C1F8F3081002ABE36 /* DPHostIPodAudioPlayer.m in Sources */, D6C860251D38D3AF00232EEF /* DPHostService.m in Sources */, + ABDC5BDD1F945AEE007201D9 /* DPHostSimpleHttpServer.m in Sources */, D69997451A7E210D00EF934F /* DPHostCanvasView.m in Sources */, D6384EAD1A7625CF00384167 /* DPHostCanvasUIViewController.m in Sources */, 252F870319736DEB00142BAE /* DPHostPhoneProfile.m in Sources */, @@ -886,6 +892,7 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "dConnectDeviceHost/DPHostDevicePlugin-Prefix.pch"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../../dConnectSDK/dConnectSDKForIOS/DConnectSDK/Dependencies/CocoaAsyncSocket"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = dConnectDeviceHost; SKIP_INSTALL = YES; @@ -903,6 +910,7 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "dConnectDeviceHost/DPHostDevicePlugin-Prefix.pch"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../../dConnectSDK/dConnectSDKForIOS/DConnectSDK/Dependencies/CocoaAsyncSocket"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = dConnectDeviceHost; SKIP_INSTALL = YES; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m index 92d8949a..8521ba67 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostMediaStreamRecordingProfile.m @@ -125,7 +125,7 @@ - (instancetype)init @"timeslice is not supported; please omit this parameter."]; return YES; } - [weakSelf.manager recordForTarget:target timeSlice:timeslice response:response completionHandler:^(NSError *error) { + [weakSelf.manager recordForTarget:target timeSlice:timeslice completionHandler:^(NSError *error) { if (!error) { [response setResult:DConnectMessageResultTypeOk]; [weakSelf sendOnRecordingChangeEventWithStatus:DConnectMediaStreamRecordingProfileRecordingStateRecording @@ -209,7 +209,6 @@ - (instancetype)init api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; - NSError *error = nil; [weakSelf.manager unmuteTrackForTarget:target error:&error]; [weakSelf runMediaStreamRecordingWithError:error @@ -261,37 +260,19 @@ - (instancetype)init }]; // API登録(didReceivePutOnDataAvailableRequest相当) - NSString *putOnDataAvailableRequestApiPath = [self apiPath: nil - attributeName: DConnectMediaStreamRecordingProfileAttrOnDataAvailable]; - [self addPutPath: putOnDataAvailableRequestApiPath + NSString *putPreviewRequestApiPath = [self apiPath: nil + attributeName: DConnectMediaStreamRecordingProfileAttrPreview]; + [self addPutPath: putPreviewRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - - NSString *serviceId = [request serviceId]; - - NSArray *evts = [[weakSelf eventMgr] eventListForServiceId:serviceId - profile:DConnectMediaStreamRecordingProfileName - attribute:DConnectMediaStreamRecordingProfileAttrOnDataAvailable]; - if (evts.count == 0) { - [weakSelf profile:weakSelf didReceivePostRecordRequest:nil response:[response copy] - serviceId:serviceId target:nil timeslice:nil]; - - // プレビュー画像URIの配送処理が開始されていないのなら、開始する。 -// _sendPreview = YES; - } - - switch ([[weakSelf eventMgr] addEventForRequest:request]) { - case DConnectEventErrorNone: // エラー無し. - [response setResult:DConnectMessageResultTypeOk]; - break; - case DConnectEventErrorInvalidParameter: // 不正なパラメータ. - [response setErrorToInvalidRequestParameter]; - break; - case DConnectEventErrorNotFound: // マッチするイベント無し. - case DConnectEventErrorFailed: // 処理失敗. - [response setErrorToUnknown]; - break; + NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; + NSError *error = nil; + NSString *uri = [weakSelf.manager startPreviewForTarget:target error:&error]; + if (!error) { + [DConnectMediaStreamRecordingProfile setUri:uri target:response]; + [response setResult:DConnectMessageResultTypeOk]; + } else { + [response setError:error.code message:error.localizedDescription]; } - return YES; }]; @@ -338,39 +319,18 @@ - (instancetype)init }]; // API登録(didReceiveDeleteOnDataAvailableRequest相当) - NSString *deleteOnDataAvailableRequestApiPath = [self apiPath: nil - attributeName: DConnectMediaStreamRecordingProfileAttrOnDataAvailable]; - [self addDeletePath: deleteOnDataAvailableRequestApiPath + NSString *deletePreviewRequestApiPath = [self apiPath: nil + attributeName: DConnectMediaStreamRecordingProfileAttrPreview]; + [self addDeletePath: deletePreviewRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) { - - NSString *serviceId = [request serviceId]; - - switch ([[weakSelf eventMgr] removeEventForRequest:request]) { - case DConnectEventErrorNone: // エラー無し. - [response setResult:DConnectMessageResultTypeOk]; - break; - case DConnectEventErrorInvalidParameter: // 不正なパラメータ. - [response setErrorToInvalidRequestParameter]; - break; - case DConnectEventErrorNotFound: // マッチするイベント無し. - case DConnectEventErrorFailed: // 処理失敗. - [response setErrorToUnknown]; - break; - } - - NSArray *evts = [[weakSelf eventMgr] eventListForServiceId:serviceId - profile:DConnectMediaStreamRecordingProfileName - attribute:DConnectMediaStreamRecordingProfileAttrOnDataAvailable]; - if (evts.count == 0) { - [weakSelf profile:weakSelf didReceivePutStopRequest:nil response:[response copy] - serviceId:serviceId target:nil]; - - // イベント受領先が存在しないなら、プレビュー画像URIの配送処理を停止する。 -// _sendPreview = NO; - // 次回プレビュー開始時に影響を与えない為に、初期値(無効値)を設定する。 -// _lastPreviewTimestamp = kCMTimeInvalid; + NSString *target = [DConnectMediaStreamRecordingProfile targetFromRequest:request]; + NSError *error = nil; + [weakSelf.manager stopPreviewForTarget:target error:&error]; + if (!error) { + [response setResult:DConnectMessageResultTypeOk]; + } else { + [response setError:error.code message:error.localizedDescription]; } - return YES; }]; @@ -448,67 +408,7 @@ - (void) sendOnRecordingChangeEventWithStatus:(NSString *)status } -- (void) sendOnDataAvailableEventWithSampleBuffer:(CMSampleBufferRef)sampleBuffer -{ - NSURL *fileURL; - NSArray *evts; - @autoreleasepool { - CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - if (!imageBuffer) { - return; - } - CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer]; - if (!ciImage) { - return; - } - - // イベントの取得 - evts = [_eventMgr eventListForServiceId:DPHostDevicePluginServiceId - profile:DConnectMediaStreamRecordingProfileName - attribute:DConnectMediaStreamRecordingProfileAttrOnDataAvailable]; - - // プレビュー画像の書き出し。 - if (evts.count > 0) { - UIImage *image = [UIImage imageWithCIImage:ciImage]; - CGSize size = image.size; - double scale = 160000.0 / (size.width * size.height); - size = CGSizeMake((int)(size.width * scale), (int)(size.height * scale)); - UIGraphicsBeginImageContext(size); - [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - NSData *jpegData = UIImageJPEGRepresentation(image, 1.0); - -// NSString *fileName = [NSString stringWithFormat:@"preview_%02d", _curPreviewImageEnumerator]; -// fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; -// if (!fileURL || ![jpegData writeToURL:fileURL atomically:NO]) { -// return; -// } - } else { - return; - } - } -// _curPreviewImageEnumerator = (_curPreviewImageEnumerator + 1) % 100; - - DConnectURIBuilder *builder = [DConnectURIBuilder new]; - builder.profile = @"files"; - [builder addParameter:[NSString stringWithFormat:@"%@", - [DPHostUtils percentEncodeString:[fileURL path] - withEncoding:NSUTF8StringEncoding]] - forName:@"uri"]; - NSString *uri = builder.build.absoluteString; - // イベント送信 - for (DConnectEvent *evt in evts) { - DConnectMessage *eventMsg = [DConnectEventManager createEventMessageWithEvent:evt]; - DConnectMessage *media = [DConnectMessage message]; - - [DConnectMediaStreamRecordingProfile setUri:uri target:media]; - [DConnectMediaStreamRecordingProfile setMIMEType:@"image/jpeg" target:media]; - [DConnectMediaStreamRecordingProfile setMedia:media target:eventMsg]; - - [SELF_PLUGIN sendEvent:eventMsg]; - } -} + #pragma mark - Private Methods diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h index f95cba01..6f643138 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.h @@ -25,7 +25,7 @@ - (void)takephotoForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler; // POST /mediastreamrecording/record -- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice response:(DConnectResponseMessage *)response completionHandler:(void (^)(NSError *error))completionHandler;; +- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice completionHandler:(void (^)(NSError *error))completionHandler;; // PUT /mediastreamrecording/stop - (void)stopForTarget:(NSString*)target completionHandler:(void (^)(NSURL *assetURL, NSError *error))completionHandler; @@ -43,8 +43,8 @@ - (void)unmuteTrackForTarget:(NSString*)target error:(NSError**)error; // PUT /mediaStreamRecording/preview -- (NSString*)startPreviewForTarget:(NSString*)target; +- (NSString*)startPreviewForTarget:(NSString*)target error:(NSError**)error; // DELETE /mediaStreamRecording/preview -- (void)stopPreviewForTarget:(NSString*)target; +- (void)stopPreviewForTarget:(NSString*)target error:(NSError**)error; @end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m index f5a47f24..743f39c5 100644 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostRecorderManager.m @@ -15,9 +15,15 @@ #import "DPHostRecorderContext.h" #import "DPHostUtils.h" #import "DPHostRecorderManager.h" +#import "DPHostSimpleHttpServer.h" @interface DPHostRecorderManager () +/*! + @brief Preview用のHTTPサーバ。 + */ +@property (nonatomic) DPHostSimpleHttpServer *httpServer; + /*! デフォルトの静止画レコーダーのID iOSデバイスによっては背面カメラが無かったりと差異があるので、 @@ -39,7 +45,10 @@ @interface DPHostRecorderManager () */ @property (nonatomic) NSNumber *currentRecorderId; - +/*! + 現在実行中のPreview + */ +@property (nonatomic) NSMutableArray *nowCurrentRecorders; /// レコーダーで使用できる静止画入力データ @property (nonatomic) NSMutableArray *photoDataSourceArr; /// レコーダーで使用できる動画入力データ @@ -162,7 +171,7 @@ - (void)takephotoForTarget:(NSString*)target completionHandler:(void (^)(NSURL * } // POST /mediastreamrecording/record -- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice response:(DConnectResponseMessage *)response completionHandler:(void (^)(NSError *))completionHandler +- (void)recordForTarget:(NSString*)target timeSlice:(NSNumber*)timeSlice completionHandler:(void (^)(NSError *))completionHandler { __block NSError *error = nil; DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:&error]; @@ -343,16 +352,97 @@ - (void)unmuteTrackForTarget:(NSString*)target error:(NSError**)error } // PUT /mediaStreamRecording/preview -- (NSString*)startPreviewForTarget:(NSString*)target +- (NSString*)startPreviewForTarget:(NSString*)target error:(NSError**)error { - // TODO: - return nil; + if (self.httpServer) { + [self.httpServer stop]; + self.httpServer = nil; + } + + self.httpServer = [DPHostSimpleHttpServer new]; + self.httpServer.listenPort = 10000; + BOOL result = [self.httpServer start]; + if (!result) { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"MJPEG Server cannot running."]; + return nil; + } + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5); + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + // Recording startはMovie用のtargetを指定しなければならないため、置き換える必要がある。 + NSRange photoBackRange = [recorder.name rangeOfString:@"photo_back"]; + NSRange photoFrontRange = [recorder.name rangeOfString:@"photo_front"]; + if (photoBackRange.location != NSNotFound && photoFrontRange.location == NSNotFound) { + target = @"2"; // movie_audio_video_back_0 + } else if (photoBackRange.location == NSNotFound && photoFrontRange.location != NSNotFound) { + target = @"3"; // movie_audio_video_front_0 + } else if (!target) { + target = @"2"; //指定されていない場合はデフォルト + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"This target does not support preview."]; + return nil; + } + __block NSError *blockError = nil; + [self recordForTarget:target timeSlice:nil completionHandler:^(NSError *err) { + if (err) { + blockError = [DPHostUtils throwsErrorCode:err.code message:err.localizedDescription]; + } + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, timeout); + *error = blockError; + [self.nowCurrentRecorders addObject:target]; + // プレビュー画像URIの配送処理が開始されていないのなら、開始する。 + self.sendPreview = YES; + NSString *url = [self.httpServer getUrl]; + if (!url) { + [self.httpServer stop]; + self.httpServer = nil; + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"MJPEG Server cannot running."]; + return nil; + } + return url; } // DELETE /mediaStreamRecording/preview -- (void)stopPreviewForTarget:(NSString*)target +- (void)stopPreviewForTarget:(NSString*)target error:(NSError**)error { - // TODO: + if (self.httpServer) { + [self.httpServer stop]; + self.httpServer = nil; + } + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5); + DPHostRecorderContext *recorder = [self recorderForTarget:target recorderType:RecorderTypeMovie error:error]; + + // Recording startはMovie用のtargetを指定しなければならないため、置き換える必要がある。 + NSRange photoBackRange = [recorder.name rangeOfString:@"photo_back"]; + NSRange photoFrontRange = [recorder.name rangeOfString:@"photo_front"]; + if (photoBackRange.location != NSNotFound && photoFrontRange.location == NSNotFound) { + target = @"2"; // movie_audio_video_back_0 + } else if (photoBackRange.location == NSNotFound && photoFrontRange.location != NSNotFound) { + target = @"3"; // movie_audio_video_front_0 + } else if (!target) { + target = @"2"; //指定されていない場合はデフォルト + } else { + *error = [DPHostUtils throwsErrorCode:DConnectMessageErrorCodeIllegalDeviceState message:@"This target does not support preview."]; + return; + } + __block NSError *blockError = nil; + [self stopForTarget:target completionHandler:^(NSURL *assetURL, NSError *err) { + if (err) { + blockError = [DPHostUtils throwsErrorCode:err.code message:err.localizedDescription]; + } + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, timeout); + *error = blockError; + [self.nowCurrentRecorders removeObject:target]; + + // イベント受領先が存在しないなら、プレビュー画像URIの配送処理を停止する。 + self.sendPreview = NO; + // 次回プレビュー開始時に影響を与えない為に、初期値(無効値)を設定する。 + self.lastPreviewTimestamp = kCMTimeInvalid; } @@ -360,6 +450,8 @@ - (void)stopPreviewForTarget:(NSString*)target #pragma mark - MediaStreamRecording Profile Init Internal - (void)initVariables { + self.httpServer = nil; + self.nowCurrentRecorders = [NSMutableArray array]; self.recorderArr = [NSMutableArray array]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(deviceOrientationDidChange) @@ -878,6 +970,9 @@ - (void)startRecordingForRecorder:(DPHostRecorderContext *)recorder error:(NSErr // ライトが点いていたら消灯する。 [self setLightOff]; + if (recorder.videoConnection.supportsVideoOrientation) { + recorder.videoConnection.videoOrientation = videoOrientationFromDeviceOrientation([UIDevice currentDevice].orientation); + } recorder.videoOrientation = [recorder.videoConnection videoOrientation]; AVCaptureDevice *captureDevice = [AVCaptureDevice deviceWithUniqueID:recorder.videoDevice.uniqueId]; @@ -970,7 +1065,7 @@ - (void)saveMovieFileForRecorder:(DPHostRecorderContext *)recorder completionHan }]; }]; } -#pragma mark - OnDataAvailable(old) +#pragma mark - Preview Internal - (BOOL) setupAssetWriterAudioInputForRecorderContext:(DPHostRecorderContext *)recorderCtx description:(CMFormatDescriptionRef)currentFormatDescription @@ -1120,6 +1215,36 @@ - (CGAffineTransform)transformVideoOrientation:(AVCaptureVideoOrientation)orient return transform; } + +- (void) sendPreviewDataWithSampleBuffer:(CMSampleBufferRef)sampleBuffer + orientation:(AVCaptureDevicePosition)orientation +{ + @autoreleasepool { + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + if (!imageBuffer) { + return; + } + CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer]; + if (!ciImage) { + return; + } + + UIImage *image = [UIImage imageWithCIImage:ciImage]; + CGSize size = image.size; + double scale = 320000.0 / (size.width * size.height); + size = CGSizeMake((int)(size.width * scale), (int)(size.height * scale)); + UIGraphicsBeginImageContext(size); + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + NSData *jpegData = UIImageJPEGRepresentation(image, 1.0); + + [self.httpServer offerData:jpegData]; + } + +} + + #pragma mark - AVCapture{Audio,Video}DataOutputSampleBufferDelegate - (void)captureOutput:(AVCaptureOutput *)captureOutput @@ -1249,21 +1374,23 @@ - (void)captureOutput:(AVCaptureOutput *)captureOutput initMuteSample = NO; } - if (!isAudio && _sendPreview && - // デフォルトカメラの時だけ - recorder == _recorderArr[[_defaultVideoRecorderId intValue]]) { - if (CMTIME_IS_INVALID(_lastPreviewTimestamp)) { - // まだプレビューの配送を行っていないのであれば、プレビューを配信する。 -// [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; - } else if (CMTIME_IS_NUMERIC(_lastPreviewTimestamp)) { - CMTime elapsedTime = - CMTimeSubtract(_lastPreviewTimestamp, originalSampleBufferTimestamp); - if (CMTIME_COMPARE_INLINE(elapsedTime, >=, _secPerFrame)) { - // 規定時間が経過したのであれば、プレビューを配信する。 -// [self sendOnDataAvailableEventWithSampleBuffer:sampleBuffer]; + if (!isAudio && _sendPreview) { + for (NSString *previewId in self.nowCurrentRecorders) { + if (recorder == self.recorderArr[[previewId intValue]]) { + if (CMTIME_IS_INVALID(_lastPreviewTimestamp)) { + // まだプレビューの配送を行っていないのであれば、プレビューを配信する。 + [self sendPreviewDataWithSampleBuffer:sampleBuffer orientation:recorder.videoDevice.position]; + } else if (CMTIME_IS_NUMERIC(_lastPreviewTimestamp)) { + CMTime elapsedTime = + CMTimeSubtract(_lastPreviewTimestamp, originalSampleBufferTimestamp); + if (CMTIME_COMPARE_INLINE(elapsedTime, >=, _secPerFrame)) { + // 規定時間が経過したのであれば、プレビューを配信する。 + [self sendPreviewDataWithSampleBuffer:sampleBuffer orientation:recorder.videoDevice.position]; + } + } else { + self.lastPreviewTimestamp = originalSampleBufferTimestamp; + } } - } else { - _lastPreviewTimestamp = originalSampleBufferTimestamp; } } diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.h b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.h new file mode 100644 index 00000000..e1dc26b4 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.h @@ -0,0 +1,56 @@ +// +// DPHostSimpleHttpServer.h +// dConnectDeviceHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + +#import + + +/*! + @brief iOS端末からの画像を配信するための簡易サーバ. + */ +@interface DPHostSimpleHttpServer : NSObject + +/*! + @brief ポート番号 + */ +@property (nonatomic) NSInteger listenPort; + +/*! + @brief プレビューを送信するためのタイムスライス. + */ +@property (nonatomic) NSInteger timeSlice; + +/*! + @brief サーバを起動します. + + @retval YSE サーバの起動に成功 + @retval NO サーバの起動に失敗 + */ +- (BOOL) start; + +/*! + @brief サーバを停止します. + */ +- (void) stop; + +/*! + @brief サーバのURLを取得します. + + @retval サーバのURL + */ +- (NSString *) getUrl; + + +/*! + @brief 配信する画像を設定します. + + @param[in] data 画像データ + */ +- (void) offerData:(NSData *)data; + +@end diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.m new file mode 100644 index 00000000..253b0a52 --- /dev/null +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostMediaStreamRecording/DPHostSimpleHttpServer.m @@ -0,0 +1,272 @@ +// +// DPHostSimpleHttpServer.m +// dConnectDeviceDPHost +// +// Copyright (c) 2017 NTT DOCOMO, INC. +// Released under the MIT license +// http://opensource.org/licenses/mit-license.php +// + +#import "DPHostSimpleHttpServer.h" +#import "GCDAsyncSocket.h" +#import + + +// HTTP通信のタイムアウトを定義 +#define HTTP_TIMEOUT 3.0 + + +/*! + @brief 現在時刻をミリ秒で取得します. + @retval 現在時刻 + */ +static uint64_t getUptimeInMilliseconds() +{ + const int64_t kOneMillion = 1000 * 1000; + static mach_timebase_info_data_t s_timebase_info; + + if (s_timebase_info.denom == 0) { + (void) mach_timebase_info(&s_timebase_info); + } + + // mach_absolute_time() returns billionth of seconds, + // so divide by one million to get milliseconds + return ((mach_absolute_time() * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom)); +} + + +#pragma mark - DPHostConnection + + +@interface DPHostConnection : NSObject + +@property (nonatomic, strong) GCDAsyncSocket *fromSocket; +@property (nonatomic) BOOL ready; +@property (nonatomic) uint64_t startTime; + +@end + + +@implementation DPHostConnection + +- (instancetype)init +{ + self = [super init]; + if (self) { + self.fromSocket = nil; + self.ready = NO; + self.startTime = 0; + } + return self; +} + +@end + + +#pragma mark - DPHostSimpleHttpServer + + +@interface DPHostSimpleHttpServer () + +@end + + +@implementation DPHostSimpleHttpServer { + GCDAsyncSocket *_listenSocket; + NSMutableArray *_connections; + NSString *_boundary; + NSString *_path; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + self.listenPort = 8080; + self.timeSlice = 100; + + _listenSocket = nil; + _connections = [NSMutableArray array]; + _boundary = @"0123456789ABCDEF"; + _path = [[NSUUID UUID] UUIDString]; + } + return self; +} + + +#pragma mark - GCDAsyncSocketDelegate Methods + +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket +{ + DPHostConnection *connection = [DPHostConnection new]; + connection.fromSocket = newSocket; + connection.ready = NO; + + @synchronized(self) { + [_connections addObject:connection]; + } + + [newSocket readDataWithTimeout:HTTP_TIMEOUT tag:0]; +} + +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + @synchronized(self) { + DPHostConnection *connection = [self foundConnection:sock]; + if (connection) { + [_connections removeObject:connection]; + } + } +} + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + NSString *headerData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if ([self parseHttpHeader:headerData]) { + [self writeHeadersToSocket:sock]; + } else { + [sock disconnect]; + } +} + +#pragma mark - Private Methods + +- (BOOL) parseHttpHeader:(NSString *)header +{ + NSString *method; + NSString *path; + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + NSArray* lines = [header componentsSeparatedByString:@"\r\n"]; + int lineIndex = 0; + + if (lines.count == 0) { + return NO; + } + + NSArray *keyValue = [lines[0] componentsSeparatedByString:@" "]; + if (keyValue && keyValue.count >= 2) { + method = [keyValue[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + path = [keyValue[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + + if (!method || !path) { + return NO; + } + + if (![[method lowercaseString] isEqualToString:@"get"]) { + return NO; + } + + if (![[path substringFromIndex:1] isEqualToString:_path]) { + return NO; + } + + // 各ヘッダーを格納 + for (; lineIndex < lines.count; lineIndex++) { + NSString *line = lines[lineIndex]; + NSArray *keyValue = [line componentsSeparatedByString:@":"]; + if (keyValue && keyValue.count == 2) { + NSString *key =[keyValue[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *value = [keyValue[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + headers[key] = value; + } + } + + return YES; +} + +- (void) writeHeadersToSocket:(GCDAsyncSocket *)socket +{ + NSString *str = @"HTTP/1.0 200 OK\r\n" + "Server: DPHostSimpleHttpServer\r\n" + "Connection: close\r\n" + "Max-Age: 0\r\n" + "Expires: 0\r\n" + "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" + "Pragma: no-cache\r\n" + "Content-Type: multipart/x-mixed-replace; boundary=%@\r\n" + "\r\n" + "--%@\r\n"; + + NSString *string = [NSString stringWithFormat:str, _boundary, _boundary]; + NSData *headerData = [string dataUsingEncoding:NSUTF8StringEncoding]; + [socket writeData:headerData withTimeout:HTTP_TIMEOUT tag:0]; + + DPHostConnection *conn = [self foundConnection:socket]; + if (conn) { + conn.ready = YES; + } +} + +- (void) sendImageData:(NSData *)imageData toSocket:(GCDAsyncSocket *)socket +{ + NSString *str = @"--%@\r\n" + "Content-Type: %@\r\n" + "Content-Length: %d\r\n" + "\r\n"; + NSString *string = [NSString stringWithFormat:str, _boundary, @"image/jpg", imageData.length]; + NSData *headerData = [string dataUsingEncoding:NSUTF8StringEncoding]; + NSData *endData = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; + [socket writeData:headerData withTimeout:HTTP_TIMEOUT tag:0]; + [socket writeData:imageData withTimeout:HTTP_TIMEOUT tag:0]; + [socket writeData:endData withTimeout:HTTP_TIMEOUT tag:0]; +} + +- (DPHostConnection *) foundConnection:(GCDAsyncSocket *)socket +{ + __block DPHostConnection *result = nil; + + [_connections enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(DPHostConnection *connection, NSUInteger idx, BOOL *stop) { + if ([connection.fromSocket.description isEqualToString:socket.description]) { + result = connection; + *stop = YES; + } + }]; + + return result; +} + + +#pragma mark - Public Methods + +- (BOOL) start +{ + NSError *error = nil; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + _listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue]; + [_listenSocket acceptOnPort:self.listenPort error:&error]; + if (error) { + NSLog(@"Failed to start a server. error=%@", error); + [self stop]; + return NO; + } + return YES; +} + +- (void) stop +{ + [_listenSocket setDelegate:nil delegateQueue:NULL]; + [_listenSocket disconnect]; + _listenSocket = nil; +} + +- (NSString *) getUrl +{ + NSString *str = @"http://localhost:%d/%@"; + return [NSString stringWithFormat:str, self.listenPort, _path]; +} + +- (void) offerData:(NSData *)data +{ + __block typeof(self) weakSelf = self; + + [_connections enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(DPHostConnection *connection, NSUInteger idx, BOOL *stop) { + uint64_t elapsed = getUptimeInMilliseconds() - connection.startTime; + if (connection.ready && elapsed > weakSelf.timeSlice) { + [weakSelf sendImageData:data toSocket:connection.fromSocket]; + connection.startTime = getUptimeInMilliseconds(); + } + }]; +} + +@end From df6b51c4158a7d2389ca3c9da1689ea841d65b88 Mon Sep 17 00:00:00 2001 From: TakayukiHoshi1984 Date: Mon, 16 Oct 2017 18:13:31 +0900 Subject: [PATCH 04/13] =?UTF-8?q?UIAlertView=E3=82=92UIAlertController?= =?UTF-8?q?=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AJInterfaces/AJNLSFControllerService.mm | 4 +- .../Sources/DPAllJoynHandler.h | 2 +- .../AJNKeyStoreListenerImpl.h | 2 +- .../Classes/DPChromecastMediaPlayerProfile.m | 4 +- .../Classes/DPChromecastNotificationProfile.m | 2 +- .../DPChromecastSettingViewController.m | 2 +- .../profile/DPHostGeolocationProfile.m | 19 +++- .../Classes/DPHueLightProfile.m | 4 +- .../dConnectDeviceHue/Classes/DPHueManager.h | 10 +- .../dConnectDeviceHue/Classes/DPHueManager.m | 18 ++-- .../Classes/DPHueSettingViewController4.h | 2 +- .../Classes/DPHueSettingViewController4.m | 70 ++++---------- .../Classes/DPHueSettingViewControllerBase.m | 20 ++-- .../Classes/util/DPHueDeviceRepeatExecutor.h | 2 +- .../Classes/util/DPHueUtil.h | 4 +- .../Resources/HueSetting_iPhone.storyboard | 95 +++++++++++-------- .../Classes/db/DPIRKitDBManager.m | 2 +- .../Classes/profile/DPIRKitLightProfile.m | 2 +- .../DPIRKitConnectionGuideViewController.m | 69 +++++++------- .../DPIRKitVirtualDeviceViewController.m | 2 +- .../DPIRKitWiFiSelectionGuideViewController.m | 65 ++++++------- .../project.pbxproj | 4 +- .../Classes/beacon/DPLinkingBeaconManager.m | 4 +- .../Classes/linking/DPLinkingDevice.m | 3 +- .../util/DPLinkingDeviceRepeatExecutor.h | 2 +- .../ui/DPLinkingDeviceViewController.m | 2 +- .../Classes/util/DPLinkingUtil.h | 4 +- .../Classes/profile/DPPebbleProfileUtil.h | 4 +- .../Classes/profile/DPPebbleProfileUtil.m | 4 +- .../page/PebbleSettingView01Controller.m | 2 +- .../page/PebbleSettingView04Controller.m | 20 ++-- .../webapi/SampleCameraEventObserver.m | 4 +- .../Classes/DPGuideViewController.m | 20 ++-- .../Classes/DPSpheroSettingViewController.m | 2 +- .../Classes/DPThetaOmnidirectionalImage.h | 2 +- .../Classes/DPThetaRoiDeliveryContext.h | 2 +- .../Classes/DPThetaRoiDeliveryContext.m | 2 +- .../BookmarkDBFramework/GHDataManager.m | 26 +++-- .../dConnectBrowserForIOS9/AppDelegate.h | 2 +- .../dConnectBrowserForIOS9/ViewController.m | 2 +- .../classes/DeviceMoreViewCell.h | 2 +- .../classes/GHBookmarkTitleCell.m | 1 + .../classes/GHDeviceSettingButtonViewCell.h | 2 +- .../classes/GHDeviceUtil.h | 2 +- .../classes/GHDeviceUtil.m | 1 - .../classes/GHFolderTitleCell.m | 1 + .../classes/GHPageViewCell.m | 1 + .../classes/GHSettingViewModel.m | 2 +- .../classes/GHToolView.m | 1 + .../classes/GuideDataViewController.h | 2 +- .../classes/TopViewModel.m | 2 +- .../Classes/event/DConnectEventSession.h | 2 +- .../event/cache/db/dao/DConnectEventDao.h | 2 +- .../Classes/localoauth/LocalOAuth2Main.m | 2 +- .../LocalOAuthAccessTokenListViewController.h | 2 +- .../LocalOAuthAccessTokenListViewController.m | 81 ++++++++-------- .../Classes/utils/DConnectEventHelper.m | 12 +-- .../DConnectServiceListViewCell.m | 1 + .../DConnectSDK/DConnectFileProfile.h | 2 +- .../DConnectSDK/DConnectSDK/DConnectManager.h | 2 +- .../DConnectSDK/DConnectMessageFactory.h | 2 +- 61 files changed, 332 insertions(+), 304 deletions(-) diff --git a/dConnectDevicePlugin/dConnectDeviceAllJoyn/dConnectDeviceAllJoyn/Sources/AJInterfaces/AJNLSFControllerService.mm b/dConnectDevicePlugin/dConnectDeviceAllJoyn/dConnectDeviceAllJoyn/Sources/AJInterfaces/AJNLSFControllerService.mm index b850cf7f..62c5146f 100644 --- a/dConnectDevicePlugin/dConnectDeviceAllJoyn/dConnectDeviceAllJoyn/Sources/AJInterfaces/AJNLSFControllerService.mm +++ b/dConnectDevicePlugin/dConnectDeviceAllJoyn/dConnectDeviceAllJoyn/Sources/AJInterfaces/AJNLSFControllerService.mm @@ -2277,7 +2277,7 @@ - (id)initWithBusAttachment:(AJNBusAttachment *)busAttachment onPath:(NSString * // add the signals to the interface description // - status = [interfaceDescription addSignalWithName:@"ControllerServiceLightingReset" inputSignature:@"" argumentNames:[NSArray arrayWithObjects: nil]]; + status = [interfaceDescription addSignalWithName:@"ControllerServiceLightingReset" inputSignature:@"" argumentNames:@[]]; if (status != ER_OK && status != ER_BUS_MEMBER_ALREADY_EXISTS) { @throw [NSException exceptionWithName:@"BusObjectInitFailed" reason:@"Unable to add signal to interface: ControllerServiceLightingReset" userInfo:nil]; @@ -5394,4 +5394,4 @@ - (void)registerLSFControllerServiceLampGroupDelegateSignalHandler:(id +#import #import "AJNKeyStoreListener.h" class AJNKeyStoreListenerImpl : public ajn::KeyStoreListener { diff --git a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastMediaPlayerProfile.m b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastMediaPlayerProfile.m index 6cdd235a..454b40e5 100755 --- a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastMediaPlayerProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastMediaPlayerProfile.m @@ -609,7 +609,7 @@ - (NSArray *)contextsBySearchingAssetsLibraryWithQuery:(NSString *)query - (BOOL)handleRequest:(DConnectRequestMessage *)request response:(DConnectResponseMessage *)response serviceId:(NSString *)serviceId - callback:(void(^)())callback + callback:(void(^)(void))callback { // パラメータチェック if (serviceId == nil) { @@ -812,7 +812,7 @@ - (void)compareOrderWithResponse:(DConnectResponseMessage *)response - (void)handleEventRequest:(DConnectRequestMessage *)request response:(DConnectResponseMessage *)response isRemove:(BOOL)isRemove - callback:(void(^)())callback + callback:(void(^)(void))callback { DConnectEventManager *mgr = [DConnectEventManager sharedManagerForClass:[DPChromecastDevicePlugin class]]; DConnectEventError error; diff --git a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastNotificationProfile.m b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastNotificationProfile.m index f33ef034..da8c5e2e 100755 --- a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastNotificationProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastNotificationProfile.m @@ -106,7 +106,7 @@ - (id)init - (BOOL)handleRequest:(DConnectRequestMessage *)request response:(DConnectResponseMessage *)response serviceId:(NSString *)serviceId - callback:(void(^)())callback + callback:(void(^)(void))callback { // パラメータチェック if (serviceId == nil) { diff --git a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastSettingViewController.m b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastSettingViewController.m index bc623c27..f6fa299c 100644 --- a/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastSettingViewController.m +++ b/dConnectDevicePlugin/dConnectDeviceChromeCast/dConnectDeviceChromecast/Classes/DPChromecastSettingViewController.m @@ -39,7 +39,7 @@ - (void)viewDidLoad self.navigationItem.leftBarButtonItem.tintColor = [UIColor whiteColor]; // 下のドットの色 - UIPageControl *pageControl = [UIPageControl appearanceWhenContainedIn:[self class], nil]; + UIPageControl *pageControl = [UIPageControl appearanceWhenContainedInInstancesOfClasses:@[[self class]]]; pageControl.pageIndicatorTintColor = [UIColor grayColor]; pageControl.currentPageIndicatorTintColor = [UIColor blackColor]; pageControl.backgroundColor = [UIColor whiteColor]; diff --git a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostGeolocationProfile.m b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostGeolocationProfile.m index c2c43a53..d19ed22e 100755 --- a/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostGeolocationProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHost/dConnectDeviceHost/Classes/profile/DPHostGeolocationProfile.m @@ -359,8 +359,23 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * break; } if (message) { - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; - [alert show]; + UIAlertController * alert = [UIAlertController + alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + + + UIAlertAction* okButton = [UIAlertAction + actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + //Handle your yes please button action here + }]; + + [alert addAction:okButton]; + UIViewController *top = [DPHostUtils topViewController]; + [top presentViewController:alert animated:YES completion:nil]; } } } diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueLightProfile.m b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueLightProfile.m index 313edd9b..397b7319 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueLightProfile.m +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueLightProfile.m @@ -104,7 +104,7 @@ - (id)init } // brightフォーマットチェック - if ([self checkBrightness:[request stringForKey:DConnectLightProfileParamBrightness]]) { + if ([weakSelf checkBrightness:[request stringForKey:DConnectLightProfileParamBrightness]]) { [weakSelf setErrRespose:response]; return YES; } @@ -159,7 +159,7 @@ - (id)init } // brightフォーマットチェック - if ([self checkBrightness:[request stringForKey:DConnectLightProfileParamBrightness]]) { + if ([weakSelf checkBrightness:[request stringForKey:DConnectLightProfileParamBrightness]]) { [weakSelf setErrRespose:response]; return YES; } diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.h b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.h index b096f2f3..20f5ea1e 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.h +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.h @@ -211,7 +211,7 @@ pushlinkAuthenticationSuccessSelector:(SEL)pushlinkAuthenticationSuccessSelector -(BOOL)changeLightStatusWithLightId:(NSString *)lightId lightState:(PHLightState*)lightState flashing:(NSArray*)flashing - completion:(void(^)())completion; + completion:(void(^)(void))completion; /*! @@ -230,7 +230,7 @@ pushlinkAuthenticationSuccessSelector:(SEL)pushlinkAuthenticationSuccessSelector color:(NSString *)color brightness:(NSNumber *)brightness flashing:(NSArray*)flashing - completion:(void(^)())completion; + completion:(void(^)(void))completion; /*! @brief ライトグループのステータスを変更する。 @@ -242,7 +242,7 @@ pushlinkAuthenticationSuccessSelector:(SEL)pushlinkAuthenticationSuccessSelector */ - (BOOL)changeGroupStatusWithGroupId:(NSString *)groupId lightState:(PHLightState*)lightState - completion:(void(^)())completion; + completion:(void(^)(void))completion; /*! @@ -255,7 +255,7 @@ pushlinkAuthenticationSuccessSelector:(SEL)pushlinkAuthenticationSuccessSelector */ -(BOOL)changeGroupNameWithGroupId:(NSString *)groupId name:(NSString *)name - completion:(void(^)())completion; + completion:(void(^)(void))completion; /*! @@ -279,7 +279,7 @@ pushlinkAuthenticationSuccessSelector:(SEL)pushlinkAuthenticationSuccessSelector @retval NO レスポンスパラメータを返却しないので、@link DConnectManager::sendResponse: @endlinkで返却すること。 */ -(BOOL)removeLightGroupWithWithGroupId:(NSString*)groupId - completion:(void(^)())completion; + completion:(void(^)(void))completion; /*! @brief 文字列の実数判定。 diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.m b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.m index d0f36e13..ba5d8e4d 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.m +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueManager.m @@ -344,7 +344,7 @@ -(BOOL)createLightGroupWithLightIds:(NSArray*)lightIds //ライトグループの削除 -(BOOL)removeLightGroupWithWithGroupId:(NSString*)groupId - completion:(void(^)())completion + completion:(void(^)(void))completion { //groupIdチェック if (![self checkParamGroupId:groupId]) { @@ -446,7 +446,7 @@ - (void)hueDeviceSearchFinished { #pragma mark - private method //completionHandlerの共通処理 -- (void) setCompletionWithResponseCompletion:(void(^)())completion +- (void) setCompletionWithResponseCompletion:(void(^)(void))completion errors:(NSArray*)errors errorState:(BridgeConnectState)errorState { @@ -591,7 +591,7 @@ - (PHLightState*) getLightStateIsOn:(BOOL)isOn } else { dBlightness = [brightness doubleValue]; } - unsigned int redValue, greenValue, blueValue; + unsigned int redValue = 0, greenValue = 0, blueValue = 0; int myBlightness; NSString *uicolor; @@ -624,7 +624,7 @@ - (PHLightState*) getLightStateIsOn:(BOOL)isOn - (BOOL) changeLightStatusWithLightId:(NSString *)lightId lightState:(PHLightState*)lightState flashing:(NSArray*)flashing - completion:(void(^)())completion + completion:(void(^)(void))completion { PHBridgeSendAPI *bridgeSendAPI = [[PHBridgeSendAPI alloc] init]; @@ -664,9 +664,9 @@ -(BOOL)changeLightNameWithLightId:(NSString *)lightId color:(NSString *)color brightness:(NSNumber *)brightness flashing:(NSArray*)flashing - completion:(void(^)())completion + completion:(void(^)(void))completion { - unsigned int redValue, greenValue, blueValue; + unsigned int redValue = 0, greenValue = 0, blueValue = 0; // 省略時はMax値(1.0)を設定する double brightness_ = 1; @@ -696,7 +696,7 @@ -(BOOL)changeLightNameWithLightId:(NSString *)lightId [self changeLightStatusWithLightId:lightId lightState:onState flashing:flashing - completion:^(NSArray *errors) { + completion:^ { // [self setCompletionWithResponseCompletion:completion // errors:errors // errorState:STATE_ERROR_CHANGE_FAIL_LIGHT_NAME]; @@ -725,7 +725,7 @@ -(BOOL)changeLightNameWithLightId:(NSString *)lightId */ - (BOOL)changeGroupStatusWithGroupId:(NSString *)groupId lightState:(PHLightState*)lightState - completion:(void(^)())completion + completion:(void(^)(void))completion { PHBridgeSendAPI *bridgeSendAPI = [[PHBridgeSendAPI alloc] init]; @@ -750,7 +750,7 @@ - (BOOL)changeGroupStatusWithGroupId:(NSString *)groupId */ - (BOOL) changeGroupNameWithGroupId:(NSString *)groupId name:(NSString *)name - completion:(void(^)())completion + completion:(void(^)(void))completion { PHBridgeSendAPI *bridgeSendAPI = [[PHBridgeSendAPI alloc] init]; PHBridgeResourcesCache *cache = [PHBridgeResourcesReader readBridgeResourcesCache]; diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.h b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.h index bf30ad46..8084911e 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.h +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.h @@ -19,6 +19,6 @@ @class DPHueSettingViewController4 @brief Hueの設定画面の4ページ目。 */ -@interface DPHueSettingViewController4 : DPHueSettingViewControllerBase +@interface DPHueSettingViewController4 : DPHueSettingViewControllerBase @end diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.m b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.m index 3671523e..3be235bb 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.m +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewController4.m @@ -131,68 +131,34 @@ - (IBAction)searchAutomatic:(id)sender { } - (IBAction)searchManual:(id)sender { - float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue]; - if (osVersion > 8.0) { - UIAlertController *serialAlert = [UIAlertController alertControllerWithTitle:DPHueLocalizedString(_bundle, @"HueSerialNoTitle") - message:DPHueLocalizedString(_bundle, @"HueSerialNoDesc") - preferredStyle:UIAlertControllerStyleAlert]; + UIAlertController *serialAlert = [UIAlertController alertControllerWithTitle:DPHueLocalizedString(_bundle, @"HueSerialNoTitle") + message:DPHueLocalizedString(_bundle, @"HueSerialNoDesc") + preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:nil]; - [serialAlert addAction:cancelAction]; - _okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - _indicator.hidden = NO; - [self startIndicator]; - NSArray *serials = @[_serial]; - [manager registerLightForSerialNo:serials completion:^(NSArray *errors) { - retryCount = 2; - lightCount = (int) [[DPHueManager sharedManager] getLightStatus].allValues.count; - [self reloadHue]; - }]; - }]; - _okAction.enabled = NO; - [serialAlert addAction:_okAction]; - - [serialAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { - - textField.placeholder = DPHueLocalizedString(_bundle, @"HueSerialNoHint"); - textField.delegate = self; - textField.keyboardType = UIKeyboardTypeAlphabet; - }]; - [self presentViewController:serialAlert animated:YES completion:nil]; - } else { - UIAlertView* alert = [[UIAlertView alloc] initWithTitle:DPHueLocalizedString(_bundle, @"HueSerialNoTitle") - message:DPHueLocalizedString(_bundle, @"HueSerialNoDesc") - delegate:self - cancelButtonTitle:@"Cancel" - otherButtonTitles:@"OK", nil]; - alert.delegate = self; - alert.alertViewStyle = UIAlertViewStylePlainTextInput; - [alert textFieldAtIndex:0].placeholder = DPHueLocalizedString(_bundle, @"HueSerialNoHint"); - [alert textFieldAtIndex:0].delegate = self; - [alert textFieldAtIndex:0].keyboardType = UIKeyboardTypeAlphabet; - [alert show]; - } -} - -- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex -{ - if( buttonIndex == alertView.cancelButtonIndex ) { return; } - - NSString* textValue = [[alertView textFieldAtIndex:0] text]; - if( [textValue length] >= 6 ) - { + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:nil]; + [serialAlert addAction:cancelAction]; + _okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { _indicator.hidden = NO; [self startIndicator]; - NSArray *serials = @[textValue]; + NSArray *serials = @[_serial]; [manager registerLightForSerialNo:serials completion:^(NSArray *errors) { retryCount = 2; lightCount = (int) [[DPHueManager sharedManager] getLightStatus].allValues.count; [self reloadHue]; }]; - } + }]; + _okAction.enabled = NO; + [serialAlert addAction:_okAction]; + + [serialAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + + textField.placeholder = DPHueLocalizedString(_bundle, @"HueSerialNoHint"); + textField.delegate = self; + textField.keyboardType = UIKeyboardTypeAlphabet; + }]; + [self presentViewController:serialAlert animated:YES completion:nil]; } - - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewControllerBase.m b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewControllerBase.m index 2204772b..6eb6b042 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewControllerBase.m +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/DPHueSettingViewControllerBase.m @@ -84,13 +84,21 @@ - (void)viewDidDisappear:(BOOL)animated - (void)showAleart:(NSString*)msg { - UIAlertView *alert = [[UIAlertView alloc] - initWithTitle:@"hue" - message:msg delegate:self - cancelButtonTitle:@"OK" - otherButtonTitles:nil]; + UIAlertController * alert = [UIAlertController + alertControllerWithTitle:@"hue" + message:msg + preferredStyle:UIAlertControllerStyleAlert]; - [alert show]; + + + UIAlertAction* okButton = [UIAlertAction + actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:nil]; + + [alert addAction:okButton]; + + [self presentViewController:alert animated:YES completion:nil]; } - (BOOL)ipad diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueDeviceRepeatExecutor.h b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueDeviceRepeatExecutor.h index f08e6803..dbd191f5 100644 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueDeviceRepeatExecutor.h +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueDeviceRepeatExecutor.h @@ -9,7 +9,7 @@ #import -typedef void (^DPSRepeateBlock)(); +typedef void (^DPSRepeateBlock)(void); @interface DPHueDeviceRepeatExecutor : NSObject diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueUtil.h b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueUtil.h index f4a27681..b564159f 100644 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueUtil.h +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Classes/util/DPHueUtil.h @@ -9,8 +9,8 @@ #import -typedef void (^DPHueUtilTimerBlock)(); -typedef void (^DPHueUtilTimerCancelBlock)(); +typedef void (^DPHueUtilTimerBlock)(void); +typedef void (^DPHueUtilTimerCancelBlock)(void); @interface DPHueUtil : NSObject diff --git a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Resources/HueSetting_iPhone.storyboard b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Resources/HueSetting_iPhone.storyboard index 3e628505..fab0dcee 100755 --- a/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Resources/HueSetting_iPhone.storyboard +++ b/dConnectDevicePlugin/dConnectDeviceHue/dConnectDeviceHue/Resources/HueSetting_iPhone.storyboard @@ -1,8 +1,11 @@ - + + + + - + @@ -12,7 +15,7 @@ - + @@ -32,7 +35,7 @@ - + @@ -63,13 +66,14 @@ - + + - + @@ -79,9 +83,10 @@ + +