diff --git a/Source/OEXVideoEncoding.h b/Source/OEXVideoEncoding.h index 3afebe19b5..03698dde48 100644 --- a/Source/OEXVideoEncoding.h +++ b/Source/OEXVideoEncoding.h @@ -12,11 +12,13 @@ NS_ASSUME_NONNULL_BEGIN @interface OEXVideoEncoding : NSObject -- (id)initWithDictionary:(NSDictionary*)dictionary; -- (id)initWithURL:(NSString*)URL size:(NSNumber*)size; +- (id)initWithDictionary:(NSDictionary*)dictionary name:(NSString*)name; +- (id)initWithName:(nullable NSString*)name URL:(NSString*)URL size:(NSNumber*)size; +@property (readonly, nonatomic, copy, nullable) NSString* name; @property (readonly, nonatomic, copy, nullable) NSString* URL; @property (readonly, nonatomic, strong, nullable) NSNumber* size; +@property (readonly, nonatomic) BOOL isYoutube; /// [String], ordered by preference + (NSArray*)knownEncodingNames; diff --git a/Source/OEXVideoEncoding.m b/Source/OEXVideoEncoding.m index c4d3bbba74..588e9327cc 100644 --- a/Source/OEXVideoEncoding.m +++ b/Source/OEXVideoEncoding.m @@ -8,8 +8,13 @@ #import "OEXVideoEncoding.h" +static NSString* const OEXVideoEncodingYoutube = @"youtube"; +static NSString* const OEXVideoEncodingMobileHigh = @"mobile_high"; +static NSString* const OEXVideoEncodingMobileLow = @"mobile_low"; + @interface OEXVideoEncoding () +@property (copy, nonatomic) NSString* name; @property (copy, nonatomic) NSString* URL; @property (strong, nonatomic) NSNumber* size; @@ -18,15 +23,16 @@ @interface OEXVideoEncoding () @implementation OEXVideoEncoding + (NSArray*)knownEncodingNames { - return @[@"mobile_low", @"mobile_high"]; + return @[OEXVideoEncodingMobileLow, OEXVideoEncodingMobileHigh, OEXVideoEncodingYoutube]; } + (NSString*)fallbackEncodingName { return @"fallback"; } -- (id)initWithDictionary:(NSDictionary*)dictionary { +- (id)initWithDictionary:(NSDictionary*)dictionary name:(NSString*)name { if(self != nil) { + self.name = name; self.URL = dictionary[@"url"]; self.size = dictionary[@"file_size"]; } @@ -35,13 +41,18 @@ - (id)initWithDictionary:(NSDictionary*)dictionary { } -- (id)initWithURL:(NSString*)URL size:(NSNumber*)size { +- (id)initWithName:(NSString*)name URL:(NSString*)URL size:(NSNumber*)size { self = [super init]; if(self != nil) { + self.name = name; self.URL = URL; self.size = size; } return self; } +- (BOOL)isYoutube { + return [self.name isEqualToString:OEXVideoEncodingYoutube]; +} + @end diff --git a/Source/OEXVideoSummary.h b/Source/OEXVideoSummary.h index 8207485eb5..731ab9f7f8 100644 --- a/Source/OEXVideoSummary.h +++ b/Source/OEXVideoSummary.h @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN +@class OEXVideoEncoding; @class OEXVideoPathEntry; @interface OEXVideoSummary : NSObject @@ -19,14 +20,17 @@ NS_ASSUME_NONNULL_BEGIN - (id)initWithDictionary:(NSDictionary*)dictionary videoID:(NSString*)videoID name:(NSString*)name; /// Generate a simple stub video summary. Used only for testing -/// path : OEXVideoPathEntry array -- (id)initWithVideoID:(NSString*)videoID name:(NSString*)name path:(NSArray*)path; +- (id)initWithVideoID:(NSString*)videoID name:(NSString*)name path:(NSArray*)path; +/// Generate a simple stub video summary. Used only for testing +- (id)initWithVideoID:(NSString*)videoID name:(NSString*)name encodings:(NSDictionary *)encodings; @property (readonly, nonatomic, copy, nullable) NSString* sectionURL; // used for OPEN IN BROWSER @property (readonly, strong, nonatomic, nullable) OEXVideoPathEntry* chapterPathEntry; @property (readonly, strong, nonatomic, nullable) OEXVideoPathEntry* sectionPathEntry; +@property (readonly, nonatomic, strong, nullable) OEXVideoEncoding* preferredEncoding; + /// displayPath : OEXVideoPathEntry array /// This is just the list [chapterPathEntry, sectionPathEntry], filtering out nil items @property (readonly, copy, nonatomic, nullable) NSArray* displayPath; diff --git a/Source/OEXVideoSummary.m b/Source/OEXVideoSummary.m index 3342547dd3..bdfff07b53 100644 --- a/Source/OEXVideoSummary.m +++ b/Source/OEXVideoSummary.m @@ -11,6 +11,7 @@ #import "edX-Swift.h" #import "OEXVideoEncoding.h" #import "OEXVideoPathEntry.h" +#import "NSMutableDictionary+OEXSafeAccess.h" #import "NSArray+OEXFunctional.h" #import "NSArray+OEXSafeAccess.h" #import "NSMutableDictionary+OEXSafeAccess.h" @@ -34,7 +35,6 @@ @interface OEXVideoSummary () // [String:OEXVideoEncoding] @property (nonatomic, strong) NSDictionary* encodings; -@property (nonatomic, strong) OEXVideoEncoding* preferredEncoding; @property (nonatomic, strong) NSDictionary* transcripts; @@ -74,18 +74,10 @@ - (id)initWithDictionary:(NSDictionary*)dictionary { NSDictionary* rawEncodings = OEXSafeCastAsClass(summary[@"encoded_videos"], NSDictionary); NSMutableDictionary* encodings = [[NSMutableDictionary alloc] init]; [rawEncodings enumerateKeysAndObjectsUsingBlock:^(NSString* name, NSDictionary* encodingInfo, BOOL *stop) { - OEXVideoEncoding* encoding = [[OEXVideoEncoding alloc] initWithDictionary:encodingInfo]; + OEXVideoEncoding* encoding = [[OEXVideoEncoding alloc] initWithDictionary:encodingInfo name:name]; [encodings safeSetObject:encoding forKey:name]; }]; - self.encodings = (rawEncodings != nil) ? encodings : @{@"fallback" : [[OEXVideoEncoding alloc] initWithURL: videoURL size:videoSize]}; - - for(NSString* name in [[OEXVideoEncoding knownEncodingNames] arrayByAddingObject:[OEXVideoEncoding fallbackEncodingName]]) { - OEXVideoEncoding* encoding = self.encodings[name]; - if (encoding != nil) { - self.preferredEncoding = encoding; - break; - } - } + self.encodings = (rawEncodings != nil) ? encodings : @{@"fallback" : [[OEXVideoEncoding alloc] initWithName:nil URL:videoURL size:videoSize]}; self.videoThumbnailURL = [summary objectForKey:@"video_thumbnail_url"]; self.videoID = [summary objectForKey:@"id"] ; @@ -119,6 +111,28 @@ - (id)initWithVideoID:(NSString*)videoID name:(NSString*)name path:(NSArray*)pat return self; } +- (id)initWithVideoID:(NSString *)videoID name:(NSString *)name encodings:(NSDictionary *)encodings { + self = [super init]; + if(self != nil) { + self.name = name; + self.videoID = videoID; + self.encodings = encodings; + } + return self; +} + +- (OEXVideoEncoding*)preferredEncoding { + for(NSString* name in [[OEXVideoEncoding knownEncodingNames] arrayByAddingObject:[OEXVideoEncoding fallbackEncodingName]]) { + OEXVideoEncoding* encoding = self.encodings[name]; + if (encoding != nil) { + return encoding; + } + } + // Don't have a known encoding, so just pick one. These are in a dict, but we need to do + // something stable, so just do it alphabetically + return self.encodings[[self.encodings.allKeys sortedArrayUsingSelector:@selector(compare:)].firstObject]; +} + - (NSString*)videoURL { return self.preferredEncoding.URL; } diff --git a/Source/VideoBlockViewController.swift b/Source/VideoBlockViewController.swift index 345fb526ac..e006677bbe 100644 --- a/Source/VideoBlockViewController.swift +++ b/Source/VideoBlockViewController.swift @@ -56,7 +56,16 @@ class VideoBlockViewController : UIViewController, CourseBlockViewController, OE func addLoadListener() { loader.listen (self, success : { [weak self] block in - if let video = self?.environment.interface?.stateForVideoWithID(self?.blockID, courseID : self?.courseID) { + if let video = block.type.asVideo, + let encoding = video.preferredEncoding where encoding.isYoutube, + let URL = encoding.URL + { + self?.showYoutubeMessage(URL) + } + else if + let video = self?.environment.interface?.stateForVideoWithID(self?.blockID, courseID : self?.courseID) + where block.type.asVideo?.preferredEncoding != nil + { self?.showLoadedBlock(block, forVideo: video) } else { @@ -215,24 +224,26 @@ class VideoBlockViewController : UIViewController, CourseBlockViewController, OE private func showError(error : NSError?) { loadController.state = LoadState.failed(error, icon: .UnknownError, message: Strings.videoContentNotAvailable) } - - private func showLoadedBlock(block : CourseBlock, forVideo video: OEXHelperVideoDownload?) { - if let _ = block.type.asVideo { - navigationItem.title = block.displayName - - dispatch_async(dispatch_get_main_queue()) { - self.loadController.state = .Loaded - } - - if let video = video { - videoController.playVideoFor(video) + + private func showYoutubeMessage(URL: String) { + let buttonInfo = MessageButtonInfo(title: Strings.Video.viewOnYoutube) { + if let URL = NSURL(string: URL) { + UIApplication.sharedApplication().openURL(URL) } } - else { - showError(nil) - } + loadController.state = LoadState.empty(icon: .CourseModeVideo, message: Strings.Video.onlyOnYoutube, attributedMessage: nil, accessibilityMessage: nil, buttonInfo: buttonInfo) } + private func showLoadedBlock(block : CourseBlock, forVideo video: OEXHelperVideoDownload) { + navigationItem.title = block.displayName + + dispatch_async(dispatch_get_main_queue()) { + self.loadController.state = .Loaded + } + + videoController.playVideoFor(video) + } + private func canDownloadVideo() -> Bool { let hasWifi = environment.reachability.isReachableViaWiFi() ?? false let onlyOnWifi = environment.dataManager.interface?.shouldDownloadOnlyOnWifi ?? false diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index e540ab5d5d..b729cec107 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -580,8 +580,12 @@ "VIDEO_ONLY_ON_WEB" = "This video is currently only on the web. Tap to view it in your web browser."; /* Alert dialog content shown when user attempts to view a video that hasn't been synced */ "VIDEO_NOT_AVAILABLE"="This video is not available in offline mode.\nPlease select a video that you've downloaded onto your device."; -/* Shows in the video setting sub-table which item is selected" */ +/* Shows in the video setting sub-table which item is selected */ "VIDEO_SETTING_SELECTED" = "✓ %@"; +/* Button action to open app on YouTube */ +"VIDEO.VIEW_ON_YOUTUBE" = "View video on YouTube"; +/* Message shown for videos that can only be played external the app via YouTube. */ +"VIDEO.ONLY_ON_YOUTUBE" = "This video can only be played on YouTube"; /* Button text for showing an associated item */ "VIEW" = "View"; /* Button linking to course announcements */ diff --git a/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_380x568@2x.png b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_380x568@2x.png new file mode 100644 index 0000000000..a56b7da7c2 Binary files /dev/null and b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_380x568@2x.png differ diff --git a/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_rtl_380x568@2x.png b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_rtl_380x568@2x.png new file mode 100644 index 0000000000..7c9f010531 Binary files /dev/null and b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios8_rtl_380x568@2x.png differ diff --git a/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_380x568@2x.png b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_380x568@2x.png new file mode 100644 index 0000000000..a56b7da7c2 Binary files /dev/null and b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_380x568@2x.png differ diff --git a/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_rtl_380x568@2x.png b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_rtl_380x568@2x.png new file mode 100644 index 0000000000..7c9f010531 Binary files /dev/null and b/Test/Snapshots/edXTests.VideoBlockViewControllerTests/testSnapshotYoutubeOnly_ios9_rtl_380x568@2x.png differ diff --git a/Test/VideoBlockViewControllerTests.swift b/Test/VideoBlockViewControllerTests.swift new file mode 100644 index 0000000000..d66034da15 --- /dev/null +++ b/Test/VideoBlockViewControllerTests.swift @@ -0,0 +1,32 @@ +// +// VideoBlockViewControllerTests.swift +// edX +// +// Created by Akiva Leffert on 3/15/16. +// Copyright © 2016 edX. All rights reserved. +// + +import Foundation + +@testable import edX + +class VideoBlockViewControllerTests : SnapshotTestCase { + + func testSnapshotYoutubeOnly() { + // Create a course with a youtube video + let summary = OEXVideoSummary(videoID: "some-video", name: "Youtube Video", encodings: [ + "youtube": OEXVideoEncoding(name: "youtube", URL: "https://some-youtube-url", size: 12)]) + let outline = CourseOutline(root: "root", blocks: [ + "root" : CourseBlock(type: CourseBlockType.Course, children: ["video"], blockID: "root", name: "Root", multiDevice: true, graded: false), + "video" : CourseBlock(type: CourseBlockType.Video(summary), children: [], blockID: "video", name: "Youtube Video", multiDevice: true, graded: false) + ]) + + let environment = TestRouterEnvironment() + environment.mockCourseDataManager.querier = CourseOutlineQuerier(courseID: "some-course", outline: outline) + + let videoController = VideoBlockViewController(environment: environment, blockID: "video", courseID: "some-course") + inScreenNavigationContext(videoController) { + assertSnapshotValidWithContent(videoController.navigationController!) + } + } +} \ No newline at end of file diff --git a/edX.xcodeproj/project.pbxproj b/edX.xcodeproj/project.pbxproj index c61150dbf9..9be93815be 100644 --- a/edX.xcodeproj/project.pbxproj +++ b/edX.xcodeproj/project.pbxproj @@ -239,6 +239,7 @@ 77503E981B2F53C300C47229 /* StreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77503E971B2F53C300C47229 /* StreamTests.swift */; }; 77509D941BE1710200B10CD3 /* TZStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77509D8F1BE1710200B10CD3 /* TZStackView.swift */; }; 77509D981BE1916600B10CD3 /* UserProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77509D971BE1916600B10CD3 /* UserProfileManager.swift */; }; + 7753914B1C98A16600FA959C /* VideoBlockViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775391491C98A15100FA959C /* VideoBlockViewControllerTests.swift */; }; 7754200A1AA763CE006FAF5B /* NSString+OEXFormatting.m in Sources */ = {isa = PBXBuildFile; fileRef = 775420091AA763CE006FAF5B /* NSString+OEXFormatting.m */; }; 775434831AD7394D00635A40 /* OEXPushNotificationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 775434821AD7394D00635A40 /* OEXPushNotificationManager.m */; }; 775434861AD73D1900635A40 /* OEXParseConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 775434851AD73D1900635A40 /* OEXParseConfig.m */; }; @@ -894,6 +895,7 @@ 77503E971B2F53C300C47229 /* StreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamTests.swift; sourceTree = ""; }; 77509D8F1BE1710200B10CD3 /* TZStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TZStackView.swift; sourceTree = ""; }; 77509D971BE1916600B10CD3 /* UserProfileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfileManager.swift; sourceTree = ""; }; + 775391491C98A15100FA959C /* VideoBlockViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoBlockViewControllerTests.swift; sourceTree = ""; }; 775420081AA763CE006FAF5B /* NSString+OEXFormatting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+OEXFormatting.h"; sourceTree = ""; }; 775420091AA763CE006FAF5B /* NSString+OEXFormatting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+OEXFormatting.m"; sourceTree = ""; }; 775434801AD738AD00635A40 /* OEXPushProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OEXPushProvider.h; sourceTree = ""; }; @@ -2616,6 +2618,7 @@ BECB7B331924C0C3009C77F1 /* Tests */ = { isa = PBXGroup; children = ( + 775391491C98A15100FA959C /* VideoBlockViewControllerTests.swift */, 7745B4001C88DD0900E76ACD /* ServerChangedCheckerTests.swift */, 7765025A1C73B90C007384E7 /* OEXRearTableViewControllerTests.swift */, 7791C4EC1C651E9D0005745B /* OEXRegistrationViewControllerTests.swift */, @@ -3577,6 +3580,7 @@ 77BECB081B0A771700894276 /* OfflineModeViewTests.swift in Sources */, 779998231C3C1EE00058E5FE /* EnrolledCoursesViewControllerTests.swift in Sources */, 194E01941A54204B00A0CFAE /* OEXInterfaceTests.m in Sources */, + 7753914B1C98A16600FA959C /* VideoBlockViewControllerTests.swift in Sources */, E0FC64C31C85B492004E3E92 /* DiscussionDataParsingTests.swift in Sources */, 46CECC441B055B0F0073C63A /* CourseDashboardViewControllerTests.swift in Sources */, 770A27AC1A702B6500DFC6FF /* OEXDataParserTests.m in Sources */,