diff --git a/Stepic/Info.plist b/Stepic/Info.plist index 19a7f8dda2..f4950d3aef 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -2,8 +2,6 @@ - UIStatusBarStyle - UIStatusBarStyleLightContent CFBundleDevelopmentRegion en CFBundleExecutable @@ -89,6 +87,10 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -97,6 +99,8 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/Stepic/Player.swift b/Stepic/Player.swift index fbf62210ff..e27c9063e5 100644 --- a/Stepic/Player.swift +++ b/Stepic/Player.swift @@ -293,6 +293,15 @@ public class Player: UIViewController { public override func viewDidLoad() { super.viewDidLoad() + if RemoteConfig.shared.allowVideoInBackground { + do { + try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("failed to set up background playing: \(error)") + } + } + self.playerView.layer.addObserver(self, forKeyPath: PlayerReadyForDisplayKey, options: ([.new, .old]), context: &PlayerLayerObserverContext) self.timeObserver = self.avplayer.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 100), queue: DispatchQueue.main, using: { [weak self] _ in guard let strongSelf = self else { return } @@ -468,28 +477,41 @@ extension Player { // UIApplication internal func addApplicationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: NSNotification.Name.UIApplicationWillResignActive, object: UIApplication.shared) - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: NSNotification.Name.UIApplicationDidEnterBackground, object: UIApplication.shared) - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: NSNotification.Name.UIApplicationWillEnterForeground, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillResignActive(_:)), name: .UIApplicationWillResignActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillEnterForeground(_:)), name: .UIApplicationWillEnterForeground, object: UIApplication.shared) } internal func removeApplicationObservers() { } - @objc internal func applicationWillResignActive(_ aNotification: NSNotification) { - if self.playbackState == .playing { + @objc internal func handleApplicationWillResignActive(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground && self.playbackState == .playing { self.pause() } } - @objc internal func applicationDidEnterBackground(_ aNotification: NSNotification) { - if self.playbackState == .playing { - self.pause() + @objc internal func handleApplicationDidBecomeActive(_ aNotification: Notification) { + if RemoteConfig.shared.allowVideoInBackground { + // Attach AVPlayer to AVPlayerLayer again + playerView.player = self.avplayer + } + } + + @objc internal func handleApplicationDidEnterBackground(_ aNotification: Notification) { + if RemoteConfig.shared.allowVideoInBackground { + // Detach AVPlayer from AVPlayerLayer (from Apple's manual) + playerView.player = nil + } else { + if self.playbackState == .playing { + self.pause() + } } } - @objc internal func applicationWillEnterForeground(_ aNoticiation: NSNotification) { - if self.playbackState == .paused { + @objc internal func handleApplicationWillEnterForeground(_ aNoticiation: Notification) { + if !RemoteConfig.shared.allowVideoInBackground && self.playbackState == .paused { self.playFromCurrentTime() } } @@ -622,21 +644,17 @@ extension Player { internal class PlayerView: UIView { - var player: AVPlayer! { + var player: AVPlayer? { get { - return (self.layer as! AVPlayerLayer).player + return playerLayer.player } set { - if (self.layer as! AVPlayerLayer).player != newValue { - (self.layer as! AVPlayerLayer).player = newValue - } + playerLayer.player = newValue } } var playerLayer: AVPlayerLayer { - get { - return self.layer as! AVPlayerLayer - } + return layer as! AVPlayerLayer } var fillMode: String { diff --git a/Stepic/RemoteConfig.swift b/Stepic/RemoteConfig.swift index a7c77f7865..7d8536b65e 100644 --- a/Stepic/RemoteConfig.swift +++ b/Stepic/RemoteConfig.swift @@ -13,10 +13,12 @@ enum RemoteConfigKeys: String { case showStreaksNotificationTrigger = "show_streaks_notification_trigger" case adaptiveBackendUrl = "adaptive_backend_url" case supportedInAdaptiveModeCourses = "adaptive_courses_ios" + case allowVideoInBackground = "allow_video_in_background" } class RemoteConfig { private let defaultShowStreaksNotificationTrigger = ShowStreaksNotificationTrigger.loginAndSubmission + private let defaultAllowVideoInBackground = false static let shared = RemoteConfig() var loadingDoneCallback: (() -> Void)? @@ -25,7 +27,8 @@ class RemoteConfig { lazy var appDefaults: [String: NSObject] = [ RemoteConfigKeys.showStreaksNotificationTrigger.rawValue: defaultShowStreaksNotificationTrigger.rawValue as NSObject, RemoteConfigKeys.adaptiveBackendUrl.rawValue: StepicApplicationsInfo.adaptiveRatingURL as NSObject, - RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepicApplicationsInfo.adaptiveSupportedCourses as NSObject + RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepicApplicationsInfo.adaptiveSupportedCourses as NSObject, + RemoteConfigKeys.allowVideoInBackground.rawValue: defaultAllowVideoInBackground as NSObject ] enum ShowStreaksNotificationTrigger: String { @@ -57,6 +60,14 @@ class RemoteConfig { return ids } + var allowVideoInBackground: Bool { + guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue(forKey: RemoteConfigKeys.allowVideoInBackground.rawValue).stringValue else { + return defaultAllowVideoInBackground + } + + return configValue == "true" + } + init() { loadDefaultValues() fetchCloudValues() diff --git a/Stepic/StepicVideoPlayerViewController.swift b/Stepic/StepicVideoPlayerViewController.swift index ec3463b4ea..c0b5378b80 100644 --- a/Stepic/StepicVideoPlayerViewController.swift +++ b/Stepic/StepicVideoPlayerViewController.swift @@ -230,6 +230,7 @@ class StepicVideoPlayerViewController: UIViewController { fileprivate var player: Player! var video: Video! + var videoInBackgroundTooltip: Tooltip? override func viewDidLoad() { super.viewDidLoad() @@ -238,6 +239,8 @@ class StepicVideoPlayerViewController: UIViewController { WatchSessionSender.sendPlaybackStatus(.available) NotificationCenter.default.addObserver(self, selector: #selector(StepicVideoPlayerViewController.audioRouteChanged(_:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared) topTimeSlider.setThumbImage(Images.playerControls.timeSliderThumb, for: UIControlState()) @@ -275,7 +278,34 @@ class StepicVideoPlayerViewController: UIViewController { topTimeSlider.addTarget(self, action: #selector(StepicVideoPlayerViewController.finishedSeeking), for: UIControlEvents.touchUpInside) topTimeSlider.addTarget(self, action: #selector(StepicVideoPlayerViewController.startedSeeking), for: UIControlEvents.touchDown) MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(self, action: #selector(StepicVideoPlayerViewController.togglePlayPause)) -// MPRemoteCommandCenter.sharedCommandCenter().togglePlayPauseCommand.addTarget(self, action: #selector(togglePlayStop(_:))); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if RemoteConfig.shared.allowVideoInBackground && TooltipDefaultsManager.shared.shouldShowInVideoPlayer { + delay(2.0) { [weak self] in + guard let s = self else { + return + } + + s.videoInBackgroundTooltip = TooltipFactory.videoInBackground + s.videoInBackgroundTooltip?.show(direction: .down, in: s.view, from: s.fullscreenPlayButton, isArrowVisible: false) + TooltipDefaultsManager.shared.didShowInVideoPlayer = true + } + } + } + + @objc internal func handleApplicationDidEnterBackground(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground { + MPRemoteCommandCenter.shared().togglePlayPauseCommand.removeTarget(self) + } + } + + @objc internal func handleApplicationDidBecomeActive(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground { + MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(self, action: #selector(StepicVideoPlayerViewController.togglePlayPause)) + } } @objc func togglePlayPause() { @@ -389,9 +419,14 @@ class StepicVideoPlayerViewController: UIViewController { extension StepicVideoPlayerViewController : PlayerDelegate { func playerReady(_ player: Player) { + guard player.playbackState == .stopped else { + return + } + print("player is ready to display") activityIndicator.isHidden = true setTimeParametersAfterPlayerIsReady() + player.seekToTime(CMTime(seconds: playerStartTime, preferredTimescale: 1000)) player.playFromCurrentTime() player.rate = currentRate.rawValue diff --git a/Stepic/Tooltip.swift b/Stepic/Tooltip.swift index 85c8e8f18d..69e9e0ce75 100644 --- a/Stepic/Tooltip.swift +++ b/Stepic/Tooltip.swift @@ -12,6 +12,7 @@ import EasyTipView protocol Tooltip { init(text: String, shouldDismissAfterTime: Bool, color: TooltipColor) func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView) + func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView, isArrowVisible: Bool) func show(direction: TooltipDirection, in inView: UIView?, from fromItem: UIBarButtonItem) func dismiss() } @@ -85,8 +86,18 @@ class EasyTipTooltip: Tooltip { preferences.drawing.borderColor = color.borderColor } - private func setupTooltip(direction: TooltipDirection) { + private func setupTooltip(direction: TooltipDirection, isArrowVisible: Bool) { preferences.drawing.arrowPosition = easyTipDirectionFromTooltipDirection(direction: direction) + + if !isArrowVisible { + switch direction { + case .up, .down: + preferences.drawing.arrowWidth = CGFloat(0) + case .left, .right: + preferences.drawing.arrowHeight = CGFloat(0) + } + } + easyTip = EasyTipView(text: text, preferences: preferences, delegate: nil) } @@ -101,13 +112,17 @@ class EasyTipTooltip: Tooltip { } func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView) { - setupTooltip(direction: direction) + show(direction: direction, in: inView, from: fromView, isArrowVisible: true) + } + + func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView, isArrowVisible: Bool) { + setupTooltip(direction: direction, isArrowVisible: isArrowVisible) easyTip.show(forView: fromView, withinSuperview: inView) setupDisappear() } func show(direction: TooltipDirection, in inView: UIView?, from fromItem: UIBarButtonItem) { - setupTooltip(direction: direction) + setupTooltip(direction: direction, isArrowVisible: true) easyTip.show(forItem: fromItem, withinSuperView: inView) setupDisappear() } diff --git a/Stepic/TooltipDefaultsManager.swift b/Stepic/TooltipDefaultsManager.swift index 50ae4637d6..a4178a90e9 100644 --- a/Stepic/TooltipDefaultsManager.swift +++ b/Stepic/TooltipDefaultsManager.swift @@ -17,6 +17,7 @@ class TooltipDefaultsManager { private let didShowOnLessonDownloadsKey = "didShowOnLessonDownloadsKey" private let didShowOnHomeContinueLearningKey = "didShowOnHomeContinueLearningKey" private let didShowOnStreaksSwitchInProfileKey = "didShowOnStreaksSwitchInProfileKey" + private let didShowInVideoPlayerKey = "didShowInVideoPlayerKey" var didShowOnLessonDownloads: Bool { set(value) { @@ -48,6 +49,16 @@ class TooltipDefaultsManager { } } + var didShowInVideoPlayer: Bool { + set(value) { + defaults.set(value, forKey: didShowInVideoPlayerKey) + } + + get { + return defaults.value(forKey: didShowInVideoPlayerKey) as? Bool ?? false + } + } + var shouldShowOnHomeContinueLearning: Bool { return !didShowOnHomeContinueLearning } @@ -59,4 +70,8 @@ class TooltipDefaultsManager { var shouldShowOnStreaksSwitchInProfile: Bool { return !didShowOnStreaksSwitchInProfile } + + var shouldShowInVideoPlayer: Bool { + return !didShowInVideoPlayer + } } diff --git a/Stepic/TooltipFactory.swift b/Stepic/TooltipFactory.swift index 8dfb75a6ea..9833e5fd42 100644 --- a/Stepic/TooltipFactory.swift +++ b/Stepic/TooltipFactory.swift @@ -24,4 +24,8 @@ struct TooltipFactory { static var streaksTooltip: Tooltip { return EasyTipTooltip(text: NSLocalizedString("StreaksSwitchTooltip", comment: ""), shouldDismissAfterTime: true, color: .standard) } + + static var videoInBackground: Tooltip { + return EasyTipTooltip(text: NSLocalizedString("VideoInBackgroundTooltip", comment: ""), shouldDismissAfterTime: true, color: .standard) + } } diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 34d38bf252..4a6767473f 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -400,6 +400,7 @@ ShareCourseTooltip = "Share the link with your friends to learn together"; LessonDownloadTooltip = "Download lesson to watch lectures offline"; ContinueLearningWidgetTooltip = "Tap to continue from where you finished last time"; StreaksSwitchTooltip = "Turn on to get new portion of knowledge every day"; +VideoInBackgroundTooltip = "You can play video in background"; /* Notification alerts */ NotificationTabNotificationRequestAlertTitle = "Stay tuned"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index cc0ae68a5e..8e61647fa6 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -401,6 +401,7 @@ ShareCourseTooltip = "Поделитесь ссылкой с друзьями, LessonDownloadTooltip = "Загрузите урок, чтобы смотреть видео оффлайн"; ContinueLearningWidgetTooltip = "Нажмите, чтобы перейти к тому месту, где закончили в прошлый раз"; StreaksSwitchTooltip = "Включите, чтобы получать новую порцию знаний каждый день"; +VideoInBackgroundTooltip = "Вы можете продолжить воспроизведение видео в фоновом режиме"; /* Notification alerts */ NotificationTabNotificationRequestAlertTitle = "Следи за обновлениями";