Skip to content

fix: Missing Source Resolution (and therefore WAB and VQ) in some cases #265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 19 commits into
base: kc/repository-reorg
Choose a base branch
from

Conversation

daytime-em
Copy link
Collaborator

@daytime-em daytime-em commented Mar 13, 2025

When we can't get a source resolution or source bitrate, we can't calculate WAB, Scaling, or other VQ-related metrics.

Cases where Src Res/Bitrate was not picked up

  • Late registration: If an AVPlayerItem or AVPlayer had been used before (as when preloading or when pooling/reusing players), we won't see the events that report things like Source URL, Source Resolution, and Advertised Bitrate
  • No Bitrate Advertised: If a media item doesn't advertise a bitrate, VQ and WAB can't be calculated. But we can calculate an effective bitrate based on the data we have. Examples of these cases: HLS streams that are just a bare MP (as discussed recently), HLS streams whose MVP doesn't have any EXT-X-BITRATE tags (they are optional), people playing progressive mp4 files (possibly; I didn't test this, could depend on what is in the mdat)

Future-facing stuff

  • It would be really great to be able to test these cases using some kind of modern, easy test harness app that allows us to make assertions in automated tests... 🤔
  • If we were listening for KVO updates on AVPlayerItem.tracks (including the .initial flag), we might be able to do some of these calculations there instead of monitorAVPlayerItem, and there'd be fewer paths that lead to this logic. Unsure if this is a real improvement or not, since the way this is written is a little more clear: "When I monitor an AVPlayerItem, I need to check for these specific things".
    • I see that as a stylistic choice, and I'm interested in others' opinions about it, if any

Draft Until

  • (possibly) until I have made some automated tests to cover this case, and removed the test code from BasicPlaybackExampleViewController
    • this one might be hard to unit test, since we depend on real behavior from a real AVPlayerItem. Unsure if it would be practical/possible to create this case in a unit test
    • also nothing about this is tested already. We do test for renditionchange, but this PR is about what happens when we can't see a rendition being selected and/or when there are no extra renditions to select.


if (accessLog && accessLog.events) {
NSArray<AVPlayerItemAccessLogEvent *> *events = [accessLog.events copy];

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something else we can do is try to scan this log for valid indicatedBitrates, though this doesn't always work

Maybe the calculation strategy below could be a fallback instead of the default behavior.. Curious what we think here, I'm pretty comfy with either option

Copy link
Contributor

@CFKevinRef CFKevinRef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggestion for the try/catch which I had already kind of thought through in setting up the cleanup tickets. Otherwise I think if we can find a way to avoid duplication this would be an even more manageable change!

Comment on lines 634 to 652
// TODO: what even throws here anyway? Is this catch-block really needed?
// @try {
for (int t = 0; t < item.tracks.count; t++) {
AVPlayerItemTrack *playerItemTrack = [
[item tracks] objectAtIndex:t
];
if (playerItemTrack) {
AVAssetTrack *assetTrack = playerItemTrack.assetTrack;
if (assetTrack) {
AVMediaType mediaType = assetTrack.mediaType;
if (mediaType && [mediaType isEqualToString:AVMediaTypeVideo]) {
return playerItemTrack;
}
}
}
}
// } @catch (NSException *exception) {
// NSLog(@"%@", exception.reason);
// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note to the other try/catch, but here it's still possible (if extremely unlikely) that the tracks property could change out from under you. In any case, for in loops are easier to write:

Suggested change
// TODO: what even throws here anyway? Is this catch-block really needed?
// @try {
for (int t = 0; t < item.tracks.count; t++) {
AVPlayerItemTrack *playerItemTrack = [
[item tracks] objectAtIndex:t
];
if (playerItemTrack) {
AVAssetTrack *assetTrack = playerItemTrack.assetTrack;
if (assetTrack) {
AVMediaType mediaType = assetTrack.mediaType;
if (mediaType && [mediaType isEqualToString:AVMediaTypeVideo]) {
return playerItemTrack;
}
}
}
}
// } @catch (NSException *exception) {
// NSLog(@"%@", exception.reason);
// }
for (AVPlayerItemTrack *playerItemTrack in item.tracks) {
if ([playerItemTrack.assetTrack.mediaType isEqual:AVMediaTypeVideo]) {
return playerItemTrack;
}
}

I also suggested here to use isEqual: instead of isEqualToString:. The only "benefit" of isEqualToString: is that it throws an exception if the argument or receiver is not a string. So it's almost always a good idea to just use isEqual:.

@@ -518,6 +571,7 @@ - (CGRect)getViewBounds {
}

- (CGSize)getSourceDimensions {
// TODO: What throws here? Is this try-catch even necessary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably since it manually iterates over _player.currentItem.tracks which means any of _player, currentItem, or tracks could all change mid-iteration. Using "fast iteration" takes care of this - behind the scenes it grabs an objectEnumerator property off the collection one time and uses that to implement the for in loop. enumerateObjectsUsingBlock: has the same benefit if you still need the index for some reason.
In any case I've flagged this for replacement in another ticket.

Comment on lines +472 to +493
NSError *error = nil;
AVKeyValueStatus status = [_playerItem.asset statusOfValueForKey:@"duration" error: &error];
if (!error && status == AVKeyValueStatusLoaded) {
CMTime duration = [_playerItem.asset duration];
float floatingSec = CMTimeGetSeconds(duration);
if (!isnan(floatingSec) && floatingSec > 0) {
float ms = floatingSec * 1000;
NSNumber *timeMs = [NSNumber numberWithFloat:ms];
[videoData setVideoSourceDuration:[NSNumber numberWithLongLong:[timeMs longLongValue]]];
}
_videoDuration = duration;
}

long alreadyAdvertisedBitrate = [self findMostRecentAdvertisedBitrateForPlayerItem:_playerItem];
if (alreadyAdvertisedBitrate > 0) {
_lastAdvertisedBitrate = alreadyAdvertisedBitrate;
_lastDispatchedAdvertisedBitrate = alreadyAdvertisedBitrate;
videoData.videoSourceAdvertisedBitrate = [
NSNumber numberWithFloat:alreadyAdvertisedBitrate
];
}
[MUXSDKCore dispatchEvent:dataEvent forPlayer:[self name]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this (and possibly the surrounding code) duplicates what's already in checkVideoData, maybe there's a way to avoid that?

Co-authored-by: Kevin Cassidy Jr <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants