Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Modules/Sources/PocketCastsDataModel/Public/Enums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ public enum PlayerAction: String, Codable, Equatable {
case transcript = "transcript"
case download = "download"
case addToPlaylist = "playlist"
case videoToggle = "video"
}

extension Array: @retroactive RawRepresentable where Element: RawRepresentable<String> {
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/PocketCastsUtils/General/AtomicBool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public class AtomicBool {

public init() {}

public init(_ initialValue: Bool) {
storageValue = initialValue
}

Comment thread
leandroalonso marked this conversation as resolved.
public var value: Bool {
get {
atomicQueue.sync {
Expand Down
3 changes: 2 additions & 1 deletion PocketCastsTests/Tests/Utilities/SettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ final class SettingsTests: XCTestCase {
.goToPodcast,
.starEpisode,
.chromecast,
.archive
.archive,
.videoToggle
]
return actions
}()
Expand Down
3 changes: 3 additions & 0 deletions podcasts/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ struct Constants {
static let currentlyPlayingEpisodeUpdated = NSNotification.Name(rawValue: "SJCurrentlyPlayingEpisodeUpdated")
static let sleepTimerChanged = NSNotification.Name(rawValue: "SJSleepTimerChanged")
static let videoPlaybackEngineSwitched = NSNotification.Name(rawValue: "SJVideoPlaybackEngineSwitched")
/// Posted when the user toggles the audio/video shelf action. Distinct from
/// `videoPlaybackEngineSwitched` (runtime video detection) so it doesn't trigger auto-open behaviour.
static let videoRenderingToggled = NSNotification.Name(rawValue: "SJVideoRenderingToggled")

// episode notifications
static let episodePlayStatusChanged = NSNotification.Name(rawValue: "SJEpPlayStatusChanged")
Expand Down
16 changes: 15 additions & 1 deletion podcasts/Enumerations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ extension PlayerAction: AnalyticsDescribable {
[
.effects, .sleepTimer, .routePicker, .shareEpisode, .addToPlaylist, .download,
.transcript, .goToPodcast, .addBookmark, .markPlayed,
.starEpisode, .chromecast, .archive
.starEpisode, .chromecast, .archive, .videoToggle
]
}

Expand Down Expand Up @@ -291,6 +291,8 @@ extension PlayerAction: AnalyticsDescribable {
self = .download
case 13:
self = .addToPlaylist
case 14:
self = .videoToggle
default:
return nil
}
Expand Down Expand Up @@ -324,6 +326,8 @@ extension PlayerAction: AnalyticsDescribable {
return 12
case .addToPlaylist:
return 13
case .videoToggle:
return 14
}
}

Expand Down Expand Up @@ -372,6 +376,8 @@ extension PlayerAction: AnalyticsDescribable {
return episode.downloaded(pathFinder: DownloadManager.shared) ? L10n.removeDownload : (episode.isInDownloadProcess ? L10n.statusDownloading : L10n.download)
case .addToPlaylist:
return L10n.playlistManualEpisodeAddToPlaylist
case .videoToggle:
return PlaybackManager.shared.shouldRenderVideo() ? L10n.playerActionHideVideo : L10n.playerActionShowVideo
}
}

Expand Down Expand Up @@ -417,6 +423,8 @@ extension PlayerAction: AnalyticsDescribable {
return episode.downloaded(pathFinder: DownloadManager.shared) ? "episode-downloaded" : "episode-download"
case .addToPlaylist:
return "playlist-add-episode"
case .videoToggle:
return PlaybackManager.shared.shouldRenderVideo() ? "video_off" : "video_on"
}
}

Expand Down Expand Up @@ -451,13 +459,17 @@ extension PlayerAction: AnalyticsDescribable {
return episode.downloaded(pathFinder: DownloadManager.shared) ? "episode-downloaded" : "episode-download"
case .addToPlaylist:
return "playlist-add-episode"
case .videoToggle:
return PlaybackManager.shared.shouldRenderVideo() ? "video_off" : "video_on"
}
}

func canBePerformedOn(episode: BaseEpisode) -> Bool {
switch self {
case .starEpisode, .shareEpisode:
return episode is Episode
case .videoToggle:
return PlaybackManager.shared.canToggleVideoRendering()
default:
return true
}
Expand Down Expand Up @@ -497,6 +509,8 @@ extension PlayerAction: AnalyticsDescribable {
return "download"
case .addToPlaylist:
return "add_to_playlist"
case .videoToggle:
return "video_toggle"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion podcasts/MiniPlayerToFullPlayerAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class MiniPlayerToFullPlayerAnimator: NSObject, UIViewControllerAnimatedTransiti
}

private var isVideoPodcast: Bool {
PlaybackManager.shared.isCurrentEpisodeVideo()
PlaybackManager.shared.shouldRenderVideo()
}
Comment thread
leandroalonso marked this conversation as resolved.
Outdated

init?(fromViewController: UIViewController, toViewController: UIViewController, transition: Transition, miniPlayerArtwork: PodcastImageView, fullPlayerArtwork: UIImageView, dismissVelocity: CGFloat = 0, fullPlayerYPosition: CGFloat = 0) {
Expand Down
22 changes: 21 additions & 1 deletion podcasts/NowPlayingPlayerItemViewController+Shelf.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ protocol NowPlayingActionsDelegate: AnyObject {
func bookmarkTapped()
func transcriptTapped()
func downloadTapped()
func videoToggleTapped()
func sharedRoutePicker(largeSize: Bool) -> PCRoutePickerView
func presentManualPlaylistsChooser()
}
Expand All @@ -33,7 +34,7 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate {
#endif

// don't reload the actions unless we need to
if !lastShelfLoadState.updateRequired(shelfActions: actions, episodeUuid: playingEpisode.uuid, effectsOn: PlaybackManager.shared.effects().effectsEnabled(), sleepTimerOn: PlaybackManager.shared.sleepTimerActive(), episodeStarred: playingEpisode.keepEpisode, episodeStatus: playingEpisode.episodeStatus) { return }
if !lastShelfLoadState.updateRequired(shelfActions: actions, episodeUuid: playingEpisode.uuid, effectsOn: PlaybackManager.shared.effects().effectsEnabled(), sleepTimerOn: PlaybackManager.shared.sleepTimerActive(), episodeStarred: playingEpisode.keepEpisode, episodeStatus: playingEpisode.episodeStatus, videoToggleAvailable: PlaybackManager.shared.canToggleVideoRendering(), videoRenderingEnabled: PlaybackManager.shared.isVideoRenderingEnabled) { return }

// load the first 4 actions into the player, followed by an overflow icon
playerControlsStackView.removeAllSubviews()
Expand Down Expand Up @@ -184,6 +185,16 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate {

addToShelf(on: button)
#endif
case .videoToggle:
let renderingVideo = PlaybackManager.shared.shouldRenderVideo()
let button = UIButton(frame: CGRect.zero)
button.isPointerInteractionEnabled = true
button.imageView?.tintColor = ThemeColor.playerContrast02()
button.setImage(UIImage(named: renderingVideo ? "video_off" : "video_on"), for: .normal)
button.addTarget(self, action: #selector(videoToggleBtnTapped(_:)), for: .touchUpInside)
button.accessibilityLabel = renderingVideo ? L10n.playerActionHideVideo : L10n.playerActionShowVideo

addToShelf(on: button)
}

return true
Expand Down Expand Up @@ -280,6 +291,10 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate {
#endif
}

func videoToggleTapped() {
PlaybackManager.shared.toggleVideoRendering()
}

func downloadTapped() {
#if !APPCLIP
guard let episode = PlaybackManager.shared.currentEpisode() as? Episode else { return }
Expand Down Expand Up @@ -415,6 +430,11 @@ extension NowPlayingPlayerItemViewController: NowPlayingActionsDelegate {
#endif
}

@objc private func videoToggleBtnTapped(_ sender: UIButton) {
shelfButtonTapped(.videoToggle)
videoToggleTapped()
}

// MARK: - Sleep Timer

@objc func sleepTimerUpdated() {
Expand Down
20 changes: 17 additions & 3 deletions podcasts/NowPlayingPlayerItemViewController+Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extension NowPlayingPlayerItemViewController {
addCustomObserver(Constants.Notifications.playbackPaused, selector: #selector(update(notification:)))
addCustomObserver(Constants.Notifications.playbackTrackChanged, selector: #selector(playbackTrackChanged))
addCustomObserver(Constants.Notifications.videoPlaybackEngineSwitched, selector: #selector(videoPlaybackEngineSwitched))
addCustomObserver(Constants.Notifications.videoRenderingToggled, selector: #selector(videoPlaybackEngineSwitched))
addCustomObserver(Constants.Notifications.podcastChaptersDidUpdate, selector: #selector(update(notification:)))
addCustomObserver(Constants.Notifications.googleCastStatusChanged, selector: #selector(update(notification:)))
addCustomObserver(Constants.Notifications.playbackEffectsChanged, selector: #selector(update(notification:)))
Expand All @@ -35,24 +36,37 @@ extension NowPlayingPlayerItemViewController {
}

@objc private func videoPlaybackEngineSwitched() {
floatingVideoView.player = PlaybackManager.shared.internalPlayerForVideoPlayback()
// Video may have been detected at runtime (e.g. an HLS stream), so refresh to reveal the view
// Video may have been detected at runtime (e.g. an HLS stream) or toggled via the shelf,
// so refresh to reveal or hide the view accordingly.
if PlaybackManager.shared.shouldRenderVideo() {
floatingVideoView.player = PlaybackManager.shared.internalPlayerForVideoPlayback()
}
update(notification: nil)
}

@objc func update(notification: NSNotification?) {
guard let playingEpisode = PlaybackManager.shared.currentEpisode() else { return }

if PlaybackManager.shared.isCurrentEpisodeVideo() {
if PlaybackManager.shared.shouldRenderVideo() {
if floatingVideoView.isHidden {
floatingVideoView.isHidden = false
floatingVideoView.player = PlaybackManager.shared.internalPlayerForVideoPlayback()
episodeImage.alpha = CGFloat.leastNonzeroMagnitude
}
} else {
let wasShowingVideo = !floatingVideoView.isHidden
floatingVideoView.player = nil
floatingVideoView.isHidden = true
episodeImage.alpha = 1.0
episodeImage.layer.opacity = 1
if wasShowingVideo {
// The artwork slot was invisible while the video was showing, so its aspect-fit
// subview may not have been laid out yet. Force a pass so the artwork appears.
if !showingCustomImage {
ImageManager.sharedManager.loadImage(episode: playingEpisode, imageView: artworkImageView, size: .page)
}
episodeImage.layoutIfNeeded()
Comment thread
leandroalonso marked this conversation as resolved.
Outdated
}
}

let skipBackAmount = Settings.skipBackTime
Expand Down
2 changes: 1 addition & 1 deletion podcasts/NowPlayingPlayerItemViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ class NowPlayingPlayerItemViewController: PlayerItemViewController {
@objc private func videoTapped() {
guard PlaybackManager.shared.currentEpisode() != nil else { return }

if PlaybackManager.shared.isCurrentEpisodeVideo() {
if PlaybackManager.shared.shouldRenderVideo() {
let videoController = VideoViewController()
videoViewController = videoController
videoViewController?.modalTransitionStyle = .crossDissolve
Expand Down
30 changes: 30 additions & 0 deletions podcasts/PlaybackManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class PlaybackManager: ServerPlaybackDelegate {
/// Atomic because it's read from now-playing updates that can run off the main queue.
private let currentStreamContainsVideo = AtomicBool()

/// Whether the video of the current stream should be rendered. Defaults to on; the user can
/// switch an HLS video stream to audio-only via the player shelf toggle. Reset per episode.
private let videoRenderingEnabled = AtomicBool(true)

private let updateTimerInterval = 1 as TimeInterval

#if !os(watchOS)
Expand Down Expand Up @@ -751,6 +755,31 @@ class PlaybackManager: ServerPlaybackDelegate {
currentEpisode()?.videoPodcast() == true || currentStreamContainsVideo.value
}

/// Whether the video surface should currently be shown. Video content can be present
/// (`isCurrentEpisodeVideo()`) while the user has chosen to listen audio-only via the shelf toggle.
func shouldRenderVideo() -> Bool {
isCurrentEpisodeVideo() && videoRenderingEnabled.value
}

var isVideoRenderingEnabled: Bool {
videoRenderingEnabled.value
}

/// Whether the audio/video toggle should be offered for the current stream. Only HLS streams
/// found to carry video (not static video podcasts) can be switched to audio-only.
func canToggleVideoRendering() -> Bool {
FeatureFlag.hls.enabled && currentStreamContainsVideo.value && (currentEpisode() is Episode)
}

/// Toggles whether the current HLS stream's video surface is shown. When disabled the player
/// shows the episode artwork instead of the video; playback and video decoding are unaffected
/// (this is a display-only switch).
func toggleVideoRendering() {
guard canToggleVideoRendering() else { return }
videoRenderingEnabled.value.toggle()
NotificationCenter.postOnMainThread(notification: Constants.Notifications.videoRenderingToggled)
}
Comment thread
leandroalonso marked this conversation as resolved.
Comment thread
leandroalonso marked this conversation as resolved.

/// Called by the player when it detects video tracks in the stream it is playing.
/// Used for HLS streams whose video content isn't reflected in the episode's file type.
func handleVideoTracksDetected(forEpisode episodeUuid: String) {
Expand Down Expand Up @@ -1419,6 +1448,7 @@ class PlaybackManager: ServerPlaybackDelegate {
private func cleanupCurrentPlayer(permanent: Bool) {
haveCalledPlayerLoad = false
currentStreamContainsVideo.value = false
videoRenderingEnabled.value = true
seekingTo = PlaybackManager.notSeeking
FileLog.shared.addMessage("cleanupCurrentPlayer permanent? \(permanent)")
if let player {
Expand Down
15 changes: 15 additions & 0 deletions podcasts/PlayerImages.xcassets/video_off.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "video_off.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions podcasts/PlayerImages.xcassets/video_on.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "video_on.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion podcasts/PlayerZoomAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class PlayerZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning
private var isInteractive: Bool { interactiveVelocity != 0 }

private var isVideoPodcast: Bool {
PlaybackManager.shared.isCurrentEpisodeVideo()
PlaybackManager.shared.shouldRenderVideo()
}
Comment thread
leandroalonso marked this conversation as resolved.
Outdated

private var presentDuration: TimeInterval { isInteractive ? 0.45 : 0.5 }
Expand Down
2 changes: 2 additions & 0 deletions podcasts/ShelfActionsViewController+Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ extension ShelfActionsViewController: UITableViewDelegate, UITableViewDataSource
self.playerActionsDelegate?.downloadTapped()
case .addToPlaylist:
self.playerActionsDelegate?.presentManualPlaylistsChooser()
case .videoToggle:
self.playerActionsDelegate?.videoToggleTapped()
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions podcasts/ShelfLoadState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ struct ShelfLoadState {
private var sleepTimerIsOn = false
private var episodeIsStarred = false
private var episodeStatus: Int32 = 0
private var videoToggleAvailable = false
private var videoRenderingEnabled = true

mutating func updateRequired(shelfActions: [PlayerAction], episodeUuid: String, effectsOn: Bool, sleepTimerOn: Bool, episodeStarred: Bool, episodeStatus: Int32) -> Bool {
if lastShelfActionsLoaded == shelfActions, lastShelfEpisodeUuid == episodeUuid, effectsAreOn == effectsOn, sleepTimerIsOn == sleepTimerOn, episodeIsStarred == episodeStarred, episodeStatus == self.episodeStatus {
mutating func updateRequired(shelfActions: [PlayerAction], episodeUuid: String, effectsOn: Bool, sleepTimerOn: Bool, episodeStarred: Bool, episodeStatus: Int32, videoToggleAvailable: Bool, videoRenderingEnabled: Bool) -> Bool {
if lastShelfActionsLoaded == shelfActions, lastShelfEpisodeUuid == episodeUuid, effectsAreOn == effectsOn, sleepTimerIsOn == sleepTimerOn, episodeIsStarred == episodeStarred, episodeStatus == self.episodeStatus, self.videoToggleAvailable == videoToggleAvailable, self.videoRenderingEnabled == videoRenderingEnabled {
return false
}

Expand All @@ -20,6 +22,8 @@ struct ShelfLoadState {
sleepTimerIsOn = sleepTimerOn
episodeIsStarred = episodeStarred
self.episodeStatus = episodeStatus
self.videoToggleAvailable = videoToggleAvailable
self.videoRenderingEnabled = videoRenderingEnabled

return true
}
Expand Down
6 changes: 6 additions & 0 deletions podcasts/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,12 @@
/* Accessibility hint text informing the user that the Sleep timer is enabled. */
"player_accessibility_sleep_timer_on" = "Sleep timer on";

/* Player action to switch an HLS stream to audio-only, hiding the video. */
"player_action_hide_video" = "Hide Video";

/* Player action to show the video of an HLS stream. */
"player_action_show_video" = "Show Video";

/* Subtitle for settings indicating this item operates as delete for files. */
"player_action_subtitle_delete" = "Shown as Delete for custom episodes";

Expand Down