diff --git a/Sources/Clappr/Classes/Plugin/Playback/AVFoundationPlayback.swift b/Sources/Clappr/Classes/Plugin/Playback/AVFoundationPlayback.swift index 510b03a2d..eed0047e2 100644 --- a/Sources/Clappr/Classes/Plugin/Playback/AVFoundationPlayback.swift +++ b/Sources/Clappr/Classes/Plugin/Playback/AVFoundationPlayback.swift @@ -549,7 +549,7 @@ open class AVFoundationPlayback: Playback { let newPosition = CMTimeGetSeconds(timeInterval.seek().time) let userInfo = ["position": newPosition] - trigger(.willSeek, userInfo: ["position": position]) + trigger(.willSeek, userInfo: userInfo) player?.currentItem?.seek(to: timeInterval) { [weak self] in self?.trigger(.didUpdatePosition, userInfo: userInfo) diff --git a/Sources/Clappr_iOS/Classes/Plugin/Container/PosterPlugin.swift b/Sources/Clappr_iOS/Classes/Plugin/Container/PosterPlugin.swift index c128a520c..d8bdf5ff4 100644 --- a/Sources/Clappr_iOS/Classes/Plugin/Container/PosterPlugin.swift +++ b/Sources/Clappr_iOS/Classes/Plugin/Container/PosterPlugin.swift @@ -78,7 +78,6 @@ open class PosterPlugin: UIContainerPlugin { if let playback = playback { listenTo(playback, eventName: Event.playing.rawValue) { [weak self] _ in self?.playbackStarted() } listenTo(playback, eventName: Event.stalling.rawValue) { [weak self] _ in self?.playbackStalled() } - listenTo(playback, eventName: Event.didComplete.rawValue) { [weak self] _ in self?.playbackEnded() } } } @@ -101,13 +100,7 @@ open class PosterPlugin: UIContainerPlugin { fileprivate func playbackStarted() { view.isHidden = true } - - fileprivate func playbackEnded() { - container?.mediaControlEnabled = false - playButton.isHidden = false - view.isHidden = false - } - + fileprivate func updatePoster(_ info: EventUserInfo) { Logger.logInfo("Updating poster", scope: pluginName) guard let posterUrl = info?[kPosterUrl] as? String else { diff --git a/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/MediaControl.swift b/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/MediaControl.swift index 8e3183f49..43870c73b 100644 --- a/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/MediaControl.swift +++ b/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/MediaControl.swift @@ -24,6 +24,7 @@ open class MediaControl: UICorePlugin, UIGestureRecognizerDelegate { private var alwaysVisible = false private var currentlyShowing = false private var currentlyHiding = false + private var isDrawerActive = false private var isChromeless: Bool { core?.options.bool(kChromeless) ?? false } required public init(context: UIObject) { @@ -72,7 +73,12 @@ open class MediaControl: UICorePlugin, UIGestureRecognizerDelegate { } listenTo(playback, eventName: Event.didComplete.rawValue) { [weak self] _ in - self?.hide() + self?.onComplete() + self?.listenToOnce(playback, eventName: Event.playing.rawValue) { [weak self] _ in + self?.show { [weak self] in + self?.disappearAfterSomeTime() + } + } } listenTo(playback, eventName: Event.didPause.rawValue) { [weak self] _ in @@ -122,16 +128,24 @@ open class MediaControl: UICorePlugin, UIGestureRecognizerDelegate { } listenTo(context, event: .didShowDrawerPlugin) { [weak self] _ in + self?.isDrawerActive = true self?.hide() } listenTo(context, event: .didHideDrawerPlugin) { [weak self] _ in - let statesToShow: [PlaybackState] = [.playing, .paused] + let statesToShow: [PlaybackState] = [.playing, .paused, .idle] + self?.isDrawerActive = false guard let state = self?.activePlayback?.state, statesToShow.contains(state) else { return } self?.show() } } + + func onComplete() { + guard !isDrawerActive else { return } + keepVisible() + show() + } func show(animated: Bool = false, completion: (() -> Void)? = nil) { if currentlyShowing || isChromeless { diff --git a/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/PlayButton.swift b/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/PlayButton.swift index bc474b139..d967c89da 100644 --- a/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/PlayButton.swift +++ b/Sources/Clappr_iOS/Classes/Plugin/Core/MediaControl/PlayButton.swift @@ -6,6 +6,7 @@ open class PlayButton: MediaControl.Element { public var playIcon = UIImage.fromName("play", for: PlayButton.self)! public var pauseIcon = UIImage.fromName("pause", for: PlayButton.self)! + public var replayIcon = UIImage.fromName("replay", for: PlayButton.self)! public var button: UIButton? { didSet { @@ -26,6 +27,12 @@ open class PlayButton: MediaControl.Element { private var canShowPauseIcon: Bool { activePlayback?.state == .playing } + + private var shouldReplay: Bool { + guard let playback = activePlayback else { return false } + + return playback.position >= playback.duration + } override open func bindEvents() { bindPlaybackEvents() @@ -38,6 +45,8 @@ open class PlayButton: MediaControl.Element { listenTo(playback, event: .playing) { [weak self] _ in self?.onPlay() } listenTo(playback, event: .stalling) { [weak self] _ in self?.hide() } listenTo(playback, event: .didStop) { [weak self] _ in self?.onStop() } + listenTo(playback, event: .didComplete) { [weak self] _ in self?.onComplete() } + listenTo(playback, event: .willSeek) { [weak self] info in self?.onWillSeek(info) } } override open func render() { @@ -71,7 +80,21 @@ open class PlayButton: MediaControl.Element { show() changeToPlayIcon() } + + private func onComplete() { + show() + changeToReplayIcon() + } + private func onWillSeek(_ info: EventUserInfo) { + guard let playback = activePlayback, + let position = info?["position"] as? Double, + position != playback.duration else { return } + + show() + canShowPlayIcon ? changeToPlayIcon() : changeToPauseIcon() + } + public func hide() { view.isHidden = true } @@ -82,7 +105,7 @@ open class PlayButton: MediaControl.Element { @objc func togglePlayPause() { guard let playback = activePlayback else { return } - + switch playback.state { case .playing: pause() @@ -98,6 +121,9 @@ open class PlayButton: MediaControl.Element { } private func play() { + if shouldReplay { + activePlayback?.seek(0) + } activePlayback?.play() } @@ -112,4 +138,8 @@ open class PlayButton: MediaControl.Element { button?.setImage(pauseIcon, for: .normal) } + + private func changeToReplayIcon() { + button?.setImage(replayIcon, for: .normal) + } } diff --git a/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/Contents.json b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/Contents.json new file mode 100644 index 000000000..6396203e0 --- /dev/null +++ b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "iconReplay.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "iconReplay@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "iconReplay@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay.png b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay.png new file mode 100644 index 000000000..0f7597e0f Binary files /dev/null and b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay.png differ diff --git a/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@2x.png b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@2x.png new file mode 100644 index 000000000..f00f7d2c2 Binary files /dev/null and b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@2x.png differ diff --git a/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@3x.png b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@3x.png new file mode 100644 index 000000000..18d86fa91 Binary files /dev/null and b/Sources/Clappr_iOS/Resources/Assets.xcassets/replay.imageset/iconReplay@3x.png differ diff --git a/Tests/Clappr_Tests/Classes/Plugin/Container/PosterPluginTests.swift b/Tests/Clappr_Tests/Classes/Plugin/Container/PosterPluginTests.swift index ec32f4ce9..733030c4f 100644 --- a/Tests/Clappr_Tests/Classes/Plugin/Container/PosterPluginTests.swift +++ b/Tests/Clappr_Tests/Classes/Plugin/Container/PosterPluginTests.swift @@ -121,11 +121,11 @@ class PosterPluginTests: QuickSpec { } context("when playback trigger a end event") { - it("reveal itself") { + it("do not reveal itself") { core.activeContainer?.playback?.trigger(Event.playing.rawValue) core.activeContainer?.playback?.trigger(Event.didComplete.rawValue) - expect(posterPlugin.view.isHidden).to(beFalse()) + expect(posterPlugin.view.isHidden).to(beTrue()) } } } diff --git a/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/MediaControlTests.swift b/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/MediaControlTests.swift index 943bfd1e4..8aa114eb5 100644 --- a/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/MediaControlTests.swift +++ b/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/MediaControlTests.swift @@ -56,8 +56,8 @@ class MediaControlTests: QuickSpec { expect(mediaControl.view.isHidden).to(beTrue()) } - context("when a option to keep media control always visible is given") { - it("doesn't hide the mediacontrol and stop timer") { + context("when kMediaControlAlwaysVisible is true") { + it("keeps the media control visible without timer") { let options: Options = [kMediaControlAlwaysVisible: true] let core = Core(options: options) let mediaControl = MediaControl(context: core) @@ -142,7 +142,7 @@ class MediaControlTests: QuickSpec { } context("when ready") { - it("shows the media control") { + it("hides the media control") { mediaControlHidden() coreStub.activePlayback?.trigger(Event.ready) @@ -153,7 +153,7 @@ class MediaControlTests: QuickSpec { } context("when playing") { - it("shows the media control") { + it("hides the media control") { mediaControlHidden() coreStub.activePlayback?.trigger(Event.playing) @@ -172,13 +172,14 @@ class MediaControlTests: QuickSpec { } context("when complete") { - it("hides the media control") { - mediaControlVisible() + it("shows the media control") { + mediaControl.view.isHidden = true + mediaControl.view.alpha = 0.0 coreStub.activePlayback?.trigger(Event.didComplete) - expect(mediaControl.view.isHidden).to(beTrue()) - expect(mediaControl.view.alpha).toEventually(equal(0)) + expect(mediaControl.view.isHidden).toEventually(beFalse()) + expect(mediaControl.view.alpha).toEventually(equal(1)) } } @@ -315,34 +316,38 @@ class MediaControlTests: QuickSpec { } context("when the drawer events are triggered") { - it("dont show Media Control when the video ended") { - var didCallShow = false + context("and the video ends") { + it("dont show Media Control") { + var didCallShow = false - coreStub.trigger(.didShowDrawerPlugin) - coreStub.activePlayback?.trigger(.didComplete) - coreStub.trigger(.didHideDrawerPlugin) + coreStub.trigger(.didShowDrawerPlugin) + coreStub.activePlayback?.trigger(.didComplete) + coreStub.trigger(.didHideDrawerPlugin) - mediaControl.show() { didCallShow = true } + mediaControl.show() { didCallShow = true } - expect(didCallShow).to(beFalse()) + expect(didCallShow).to(beFalse()) + } } - it("shows Media Control when the video is not ended") { - var didCallShow = false - - coreStub.playbackMock?.set(state: .playing) - - coreStub.trigger(.didShowDrawerPlugin) - coreStub.trigger(.didHideDrawerPlugin) - - mediaControl.show() { didCallShow = true } - - expect(didCallShow).to(beTrue()) + context("and the video does not end") { + it("shows Media Control") { + var didCallShow = false + + coreStub.playbackMock?.set(state: .playing) + + coreStub.trigger(.didShowDrawerPlugin) + coreStub.trigger(.didHideDrawerPlugin) + + mediaControl.show() { didCallShow = true } + + expect(didCallShow).to(beTrue()) + } } } func mediaControlHidden() { - coreStub.activePlayback?.trigger(Event.didComplete) + mediaControl.hide() } func mediaControlVisible() { diff --git a/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/PlayButtonTests.swift b/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/PlayButtonTests.swift index 16b926d82..ee2738d8a 100644 --- a/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/PlayButtonTests.swift +++ b/Tests/Clappr_Tests/Classes/Plugin/Core/MediaControl/PlayButtonTests.swift @@ -12,6 +12,7 @@ class PlayButtonTests: QuickSpec { beforeEach { coreStub = CoreStub() playButton = PlayButton(context: coreStub) + playButton.render() } describe("Plugin structure") { @@ -43,8 +44,6 @@ class PlayButtonTests: QuickSpec { describe("when a video is loaded") { context("and video is vod") { it("shows button") { - playButton.render() - coreStub.activeContainer?.trigger(.stalling) expect(playButton.view.isHidden).to(beFalse()) @@ -52,11 +51,7 @@ class PlayButtonTests: QuickSpec { } } - context("when click on button") { - beforeEach { - playButton.render() - } - + context("when clicked") { context("and enters in background and receive a didPause event") { it("shows play button") { coreStub.activePlayback?.trigger(.didPause) @@ -64,24 +59,6 @@ class PlayButtonTests: QuickSpec { expect(playButton.view.isHidden).toEventually(beFalse()) } } - - context("and a video is paused") { - beforeEach { - coreStub.playbackMock?.set(state: .paused) - } - - it("calls the playback play") { - playButton.button?.sendActions(for: .touchUpInside) - - expect(coreStub.playbackMock?.didCallPlay).to(beTrue()) - } - - it("shows play button") { - playButton.button?.sendActions(for: .touchUpInside) - - expect(playButton.view.isHidden).toEventually(beFalse()) - } - } context("and a video is idle") { beforeEach { @@ -102,13 +79,8 @@ class PlayButtonTests: QuickSpec { } } - context("when click on button during playback") { - beforeEach { - playButton.render() - } - - context("and a video is playing") { - + context("when clicked during playback") { + context("and video is playing") { beforeEach { coreStub.playbackMock?.set(state: .playing) } @@ -119,7 +91,7 @@ class PlayButtonTests: QuickSpec { expect(coreStub.playbackMock?.didCallPause).to(beTrue()) } - it("changes the image to a play icon") { + it("changes the icon to play") { let playIcon = UIImage.fromName("play", for: PlayButton.self)! playButton.button?.sendActions(for: .touchUpInside) @@ -128,11 +100,8 @@ class PlayButtonTests: QuickSpec { expect(currentButtonIcon.isEqual(playIcon)).toEventually(beTrue()) } - context("and is vod") { + context("and video is vod") { it("shows button") { - let coreStub = CoreStub() - let playButton = PlayButton(context: coreStub) - playButton.render() playButton.view.isHidden = true coreStub.activePlayback?.trigger(.playing) @@ -142,7 +111,7 @@ class PlayButtonTests: QuickSpec { } } - context("and a video is paused") { + context("and video is paused") { beforeEach { coreStub.playbackMock?.set(state: .paused) } @@ -153,7 +122,7 @@ class PlayButtonTests: QuickSpec { expect(coreStub.playbackMock?.didCallPlay).to(beTrue()) } - it("changes the image to a pause icon") { + it("changes the icon to pause") { let pauseIcon = UIImage.fromName("pause", for: PlayButton.self)! playButton.button?.sendActions(for: .touchUpInside) @@ -162,11 +131,8 @@ class PlayButtonTests: QuickSpec { expect(currentButtonIcon.isEqual(pauseIcon)).toEventually(beTrue()) } - context("and is vod") { + context("and video is vod") { it("shows button") { - let coreStub = CoreStub() - let playButton = PlayButton(context: coreStub) - playButton.render() playButton.view.isHidden = true coreStub.activePlayback?.trigger(.didPause) @@ -175,25 +141,33 @@ class PlayButtonTests: QuickSpec { } } } + + context("and the video has ended") { + it("restarts the video") { + coreStub.playbackMock?.set(state: .idle) + coreStub.playbackMock?.trigger(.didComplete) + coreStub.playbackMock?.set(position: 20.0) + coreStub.playbackMock?.videoDuration = 20.0 + + playButton.button?.sendActions(for: .touchUpInside) + + expect(coreStub.playbackMock?.didCallSeek).to(beTrue()) + expect(coreStub.playbackMock?.didCallSeekWithValue).to(equal(0)) + } + } } describe("render") { it("set's acessibilityIdentifier to button") { - playButton.render() - expect(playButton.button?.accessibilityIdentifier).to(equal("PlayPauseButton")) } describe("button") { it("adds it in the view") { - playButton.render() - expect(playButton.view.subviews).to(contain(playButton.button)) } it("has scaleAspectFit content mode") { - playButton.render() - expect(playButton.button?.imageView?.contentMode).to(equal(UIView.ContentMode.scaleAspectFit)) } } @@ -216,30 +190,93 @@ class PlayButtonTests: QuickSpec { describe("#events") { context("didStop") { it("shows button view") { - let core = CoreStub() - let playButton = PlayButton(context: core) - playButton.render() playButton.hide() - core.activePlayback?.trigger(.didStop) + coreStub.activePlayback?.trigger(.didStop) expect(playButton.view.isHidden).to(beFalse()) } - it("changes button icon") { - let core = CoreStub() + it("changes icon to play") { let playIcon = UIImage.fromName("play", for: PlayButton.self) - let playButton = PlayButton(context: core) - playButton.render() - core.playbackMock?.state = .playing - core.activePlayback?.trigger(.playing) - core.playbackMock?.state = .idle - core.activePlayback?.trigger(.didStop) + coreStub.activePlayback?.trigger(.didStop) expect(playButton.button?.imageView?.image).to(equal(playIcon)) } } + + context("didComplete") { + it("shows button view") { + playButton.hide() + + coreStub.activePlayback?.trigger(.didComplete) + + expect(playButton.view.isHidden).to(beFalse()) + } + + it("changes icon to replay") { + let replayIcon = UIImage.fromName("replay", for: PlayButton.self) + + coreStub.activePlayback?.trigger(.didComplete) + + expect(playButton.button?.imageView?.image).to(equal(replayIcon)) + } + } + + context("willSeek") { + var info: EventUserInfo! + beforeEach { + coreStub.playbackMock?.state = .idle + coreStub.playbackMock?.videoDuration = 20.0 + coreStub.playbackMock?.set(position: 20.0) + info = ["position": 10.0] + + } + + it("shows button view") { + playButton.hide() + + coreStub.activePlayback?.trigger(.willSeek, userInfo: info) + + expect(playButton.view.isHidden).to(beFalse()) + } + + context("from the end to another point of the video") { + it("changes icon to play") { + let playIcon = UIImage.fromName("play", for: PlayButton.self) + coreStub.playbackMock?.trigger(.didComplete) + + coreStub.playbackMock?.trigger(.willSeek, userInfo: info) + + expect(playButton.button?.imageView?.image).to(equal(playIcon)) + } + } + + context("to any point other than the end") { + context("when playing") { + it("keeps the pause icon") { + coreStub.playbackMock?.state = .playing + let playIcon = UIImage.fromName("pause", for: PlayButton.self) + + coreStub.playbackMock?.trigger(.willSeek, userInfo: info) + + expect(playButton.button?.imageView?.image).to(equal(playIcon)) + } + } + + context("when paused") { + it("keeps the play icon") { + coreStub.playbackMock?.state = .paused + let playIcon = UIImage.fromName("play", for: PlayButton.self) + + coreStub.playbackMock?.trigger(.willSeek, userInfo: info) + + expect(playButton.button?.imageView?.image).to(equal(playIcon)) + } + } + } + } } } } diff --git a/Tests/Clappr_Tests/Classes/Plugin/Playback/AVFoundationPlaybackTests.swift b/Tests/Clappr_Tests/Classes/Plugin/Playback/AVFoundationPlaybackTests.swift index 67ac4065c..5bde7ecec 100644 --- a/Tests/Clappr_Tests/Classes/Plugin/Playback/AVFoundationPlaybackTests.swift +++ b/Tests/Clappr_Tests/Classes/Plugin/Playback/AVFoundationPlaybackTests.swift @@ -1208,10 +1208,9 @@ class AVFoundationPlaybackTests: QuickSpec { let initialSeekPosition = 0.0 playback.on(Event.willSeek.rawValue) { userInfo in - position = userInfo?["position"] as? Double didTriggerWillSeek = true } - + position = playback.position playback.seek(5) expect(position).to(equal(initialSeekPosition))