Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
32deec2
Add a isNumeric check to prevent a crash
emilylaguna Jun 12, 2023
3f8007d
Update CHANGELOG.md
emilylaguna Jun 13, 2023
2c98d0c
Throw a playback error if the AVAudioFile.length returns 0 when loadi…
emilylaguna Jun 17, 2023
cf19c0e
Provide more context about throwing the effects player frame count error
emilylaguna Jun 21, 2023
16ce5b0
Add playbackFailed to the analytics events
emilylaguna Jun 21, 2023
9e3e908
Change PlaybackError to conform to LocalizedError
emilylaguna Jun 21, 2023
59f8601
Throw a more specific effectsPlayerFrameCountZero error from the effe…
emilylaguna Jun 21, 2023
0709508
Add a playbackFailed method to the AnalyticsPlaybackHelper
emilylaguna Jun 21, 2023
474236e
Set the userInitiated flag to false on the pause method when a playba…
emilylaguna Jun 21, 2023
614b02e
Track when a playback error occurs
emilylaguna Jun 21, 2023
446326f
Add more specific information if the DefaultPlayer fails to play
emilylaguna Jun 21, 2023
891822d
Remove the playback_failed analytics source since its been replaced b…
emilylaguna Jun 21, 2023
e0011ae
Keep track of the previous analytics source
emilylaguna Jun 21, 2023
01a115b
If there is no analytics source on playback fail, then revert to the …
emilylaguna Jun 21, 2023
ef3a51d
Add AnalyticsDescribable support to the players
emilylaguna Jun 21, 2023
1611c68
Pass the current player to the playbackFailed event
emilylaguna Jun 21, 2023
7e7730d
Move the analytics source fallback to the analytics coordinator
emilylaguna Jun 27, 2023
e04fc56
Add current source fallback tests
emilylaguna Jun 27, 2023
9ff7847
Fix lint
emilylaguna Jun 27, 2023
2bde5cc
Merge pull request #918 from Automattic/task/add-playback-failed-events
emilylaguna Jul 6, 2023
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 CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
7.41
-----
- Improve support section to help the user to send Apple Watch logs
- Fixed a crash that could occur when playing some downloaded episodes (#901)

7.40
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import XCTest

@testable import podcasts

final class AnalyticsCoordinatorTests: XCTestCase {
func testFallsBackToPreviousSourceOnNilCurrentSource() {
let coordinator = AnalyticsCoordinator()
coordinator.currentSource = .carPlay

// Resets the current source and sets the previous source
let previousSource = coordinator.currentAnalyticsSource

// The current source is expected to be nil, so this should reset to the original source value
coordinator.fallbackToPreviousSourceIfNeeded()

XCTAssertEqual(previousSource, coordinator.currentSource)
}

func testFallsBackToPreviousSourceOnUnknownCurrentSource() {
let coordinator = AnalyticsCoordinator()
coordinator.currentSource = .carPlay

// Resets the current source and sets the previous source
let _ = coordinator.currentAnalyticsSource
coordinator.currentSource = .unknown

// The current source is expected to be nil, so this should reset to the original source value
coordinator.fallbackToPreviousSourceIfNeeded()

XCTAssertEqual(coordinator.currentSource, .carPlay)
}

func testFallbackDoesntHappenIfCurrentSourceIsSet() {
let coordinator = AnalyticsCoordinator()
coordinator.currentSource = .carPlay

// Resets the current source and sets the previous source
let _ = coordinator.currentAnalyticsSource
coordinator.currentSource = .chooseFolder

// The current source is set, this should do nothing
coordinator.fallbackToPreviousSourceIfNeeded()

XCTAssertEqual(coordinator.currentSource, .chooseFolder)
}
}
4 changes: 4 additions & 0 deletions podcasts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,7 @@
C7C4CAEE28AB0BF200CFC8CF /* TracksSubscriptionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C4CAED28AB0BF200CFC8CF /* TracksSubscriptionData.swift */; };
C7C4CAF328ABFD0900CFC8CF /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C4CAF228ABFD0900CFC8CF /* Notifications.swift */; };
C7CA0559293E8918000E41BD /* HolographicEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CA0558293E8918000E41BD /* HolographicEffect.swift */; };
C7CDE14E2A4B41BF0081E7FF /* AnalyticsCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CDE14D2A4B41BF0081E7FF /* AnalyticsCoordinatorTests.swift */; };
C7CE415A28CBCFC200AD063E /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7AF5790289D87CF0089E435 /* Analytics.swift */; };
C7CE415B28CBD00A00AD063E /* AnalyticsEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72CED2C289DA14F0017883A /* AnalyticsEvent.swift */; };
C7CE415C28CBD01F00AD063E /* String+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72CED2E289DA1650017883A /* String+Analytics.swift */; };
Expand Down Expand Up @@ -3166,6 +3167,7 @@
C7C4CAED28AB0BF200CFC8CF /* TracksSubscriptionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksSubscriptionData.swift; sourceTree = "<group>"; };
C7C4CAF228ABFD0900CFC8CF /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
C7CA0558293E8918000E41BD /* HolographicEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HolographicEffect.swift; sourceTree = "<group>"; };
C7CDE14D2A4B41BF0081E7FF /* AnalyticsCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsCoordinatorTests.swift; sourceTree = "<group>"; };
C7D6551328E5153200AD7174 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
C7D813782A0D4B89007F715F /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
C7D813792A0D4B89007F715F /* AppIcon-Patron-Chrome-ipad-pro.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-Patron-Chrome-ipad-pro.png"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6797,6 +6799,7 @@
children = (
C7D854F228ADD98700877E87 /* AppLifecyleAnalyticsTests.swift */,
8BA55A0F28CA6843002BECC5 /* AnalyticsPlaybackHelperTests.swift */,
C7CDE14D2A4B41BF0081E7FF /* AnalyticsCoordinatorTests.swift */,
);
path = Analytics;
sourceTree = "<group>";
Expand Down Expand Up @@ -8115,6 +8118,7 @@
buildActionMask = 2147483647;
files = (
8B317BA528906CAB00A26A13 /* TestingSceneDelegate.swift in Sources */,
C7CDE14E2A4B41BF0081E7FF /* AnalyticsCoordinatorTests.swift in Sources */,
8B2319432902CF170001C3DE /* EndOfYearManagerMock.swift in Sources */,
8B68C1D829421E3400CF25C5 /* FeatureFlagTests.swift in Sources */,
8B317BA328906C8100A26A13 /* TestingAppDelegate.swift in Sources */,
Expand Down
21 changes: 21 additions & 0 deletions podcasts/Analytics/AnalyticsDescribable+Modules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,24 @@ extension SocialAuthProvider: AnalyticsDescribable {
}
}
}

// MARK: - Players
extension DefaultPlayer: AnalyticsDescribable {
var analyticsDescription: String {
"default"
}
}

#if !os(watchOS)
extension EffectsPlayer: AnalyticsDescribable {
var analyticsDescription: String {
"effects"
}
}

extension GoogleCastPlayer: AnalyticsDescribable {
var analyticsDescription: String {
"google_cast"
}
}
#endif
1 change: 1 addition & 0 deletions podcasts/Analytics/AnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ enum AnalyticsEvent: String {

case playbackPlay
case playbackPause
case playbackFailed
case playbackSkipBack
case playbackSkipForward
case playbackSeek
Expand Down
16 changes: 14 additions & 2 deletions podcasts/Analytics/Helpers/AnalyticsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ enum AnalyticsSource: String, AnalyticsDescribable {
case upNext = "up_next"
case userEpisode = "user_episode"
case videoPlayerSkipForwardLongPress = "video_player_skip_forward_long_press"
case playbackFailed = "playback_failed"
case watch
case unknown

Expand All @@ -52,9 +51,22 @@ class AnalyticsCoordinator {
/// Sometimes the playback source can't be inferred, just inform it here
var currentSource: AnalyticsSource?

/// Keep track of the analytics source that was used for the last event before it was reset
var previousSource: AnalyticsSource?

/// If the current source is not available, attempt to fallback to previous value before it was reset
func fallbackToPreviousSourceIfNeeded() {
guard currentSource == nil || currentSource == .unknown else {
return
}

currentSource = previousSource
}

#if !os(watchOS)
var currentAnalyticsSource: AnalyticsSource {
if let currentSource = currentSource {
if let currentSource {
previousSource = currentSource
self.currentSource = nil
return currentSource
}
Expand Down
4 changes: 4 additions & 0 deletions podcasts/Analytics/Helpers/AnalyticsPlaybackHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class AnalyticsPlaybackHelper: AnalyticsCoordinator {
track(.playbackSkipForward)
}

func playbackFailed(errorMessage: String, episodeUuid: String, player: PlaybackProtocol?) {
track(.playbackFailed, properties: [ "error": errorMessage, "episode_uuid": episodeUuid, "player": player ?? "unknown" ])
}

func seek(from: TimeInterval, to: TimeInterval, duration: TimeInterval) {
// Currently ignore a seek event that is triggered by a sync process
// Using the skip buttons triggers a seek, ignore this as well
Expand Down
5 changes: 5 additions & 0 deletions podcasts/AudioReadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@ class AudioReadTask {
let totalSeconds = totalFrames / audioFile.fileFormat.sampleRate
let percentSeek = time / totalSeconds

// Ignore any invalid values
guard percentSeek.isNumeric else {
return(0, false)
}

return (Int64(totalFrames * percentSeek), percentSeek >= 1)
}
}
5 changes: 4 additions & 1 deletion podcasts/DefaultPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ class DefaultPlayer: PlaybackProtocol, Hashable {

private func playerStatusDidChange() {
if player?.currentItem?.status == .failed {
PlaybackManager.shared.playbackDidFail(logMessage: "AVPlayerItemStatusFailed on currentItem", userMessage: nil)
// Attempt to provide more specific information about the error if its available
// This only returns the domain and code to help normalize it across different languages
let message = (player?.currentItem?.error as? NSError).map { "Domain: \($0.domain) - Code: \($0.code)"}
PlaybackManager.shared.playbackDidFail(logMessage: message ?? "AVPlayerItemStatusFailed on currentItem", userMessage: nil)

return
}
Expand Down
9 changes: 9 additions & 0 deletions podcasts/EffectsPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ class EffectsPlayer: PlaybackProtocol, Hashable {
// we haven't cached a frame count for this episode, do that now
strongSelf.cachedFrameCount = strongSelf.audioFile!.length
DataManager.sharedManager.saveFrameCount(episode: episode, frameCount: strongSelf.cachedFrameCount)

// Throw an error if the frame count is 0
// `cachedFrameCount` is used to calculate the duration of the episode and a 0 value
// will result in the episode immediately being marked as played or can cause Inf/NaN issues.
//
// For more info, see: https://github.com/Automattic/pocket-casts-ios/issues/900
if strongSelf.cachedFrameCount == 0 {
throw PlaybackError.effectsPlayerFrameCountZero
}
}
} catch {
objc_sync_exit(strongSelf.playerLock)
Expand Down
12 changes: 9 additions & 3 deletions podcasts/PlaybackManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,10 @@ class PlaybackManager: ServerPlaybackDelegate {

func playbackDidFail(logMessage: String?, userMessage: String?) {
FileLog.shared.addMessage("playbackDidFail: \(logMessage ?? "No error provided")")
AnalyticsPlaybackHelper.shared.currentSource = .playbackFailed

// If there is no current analytics source, then use the previous one
// This helps prevent an `unknown` from being used if this is called right after another event, such as playbackPlay
analyticsPlaybackHelper.fallbackToPreviousSourceIfNeeded()

guard let episode = currentEpisode() else {
endPlayback()
Expand All @@ -828,7 +831,11 @@ class PlaybackManager: ServerPlaybackDelegate {
// - Is the duration actually reasonable?
// if either of these is false, flag it as an error, otherwise we got close enough to the end
if episode.playedUpTo < 1.minutes || episode.duration <= 0 || ((episode.playedUpTo + 3.minutes) < episode.duration) {
pause()
analyticsPlaybackHelper.playbackFailed(errorMessage: logMessage ?? "Unknown",
episodeUuid: episode.uuid,
player: player)

pause(userInitiated: false)
NotificationCenter.postOnMainThread(notification: Constants.Notifications.playbackPaused)

if episode.downloaded(pathFinder: DownloadManager.shared) {
Expand All @@ -841,7 +848,6 @@ class PlaybackManager: ServerPlaybackDelegate {
}

NotificationCenter.postOnMainThread(notification: Constants.Notifications.playbackFailed)

return
}

Expand Down
14 changes: 13 additions & 1 deletion podcasts/PlaybackProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,19 @@ import PocketCastsDataModel
func internalPlayerForVideoPlayback() -> AVPlayer?
}

enum PlaybackError: Error {
enum PlaybackError: LocalizedError {
case unableToOpenFile
case effectsPlayerFrameCountZero
case errorDuringPlayback

var errorDescription: String? {
switch self {
case .unableToOpenFile:
return "PlaybackError: unableToOpenFile"
case .effectsPlayerFrameCountZero:
return "EffectsPlayer frameCount was 0 while opening the file"
case .errorDuringPlayback:
return "PlaybackError: errorDuringPlayback"
}
}
}