Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
db9cb79
feat: add QoE KPI constants and configuration options
avinash-newrelic Mar 11, 2026
6c9e1d6
feat: add NRQoEAggregator for KPI computation
avinash-newrelic Mar 11, 2026
669e8d3
feat: integrate QoE event injection into harvest cycle
avinash-newrelic Mar 11, 2026
12260ac
feat: wire QoE aggregator into tracker and facade
avinash-newrelic Mar 11, 2026
0f079c2
fix: replace broken video URLs in SimplePlayerWithAds example
avinash-newrelic Mar 11, 2026
bcc3423
docs: add QOE_AGGREGATE action and kpi.* attributes to DATAMODEL.md
avinash-newrelic Mar 11, 2026
4be42eb
fix: align rebuffering with Android/JS — skip initial buffer, count a…
avinash-newrelic Mar 11, 2026
04ab8fe
refactor: rename kpi.hadStartupFailure/hadPlaybackFailure to hadStart…
avinash-newrelic Mar 11, 2026
4ad8fbf
fix: check bufferType == "initial" instead of boolean flag for rebuff…
avinash-newrelic Mar 11, 2026
6fb78ab
refactor: remove kpi. prefix from QoE attribute names
avinash-newrelic Mar 11, 2026
9a2dc39
fix: use wall-clock ad duration (totalPreRollAdTime) for startup time…
avinash-newrelic Mar 13, 2026
26149ba
fix: pause bitrate timer during non-play states for accurate average …
avinash-newrelic Mar 13, 2026
79faedc
fix: move viewIdIndex increment to sendRequest so post-roll ads share…
avinash-newrelic Mar 13, 2026
f01cf59
refactor: use whitelist for QOE_AGGREGATE event attributes
avinash-newrelic Mar 13, 2026
6fd1914
feat: decouple QoE from VideoAction gate, add snapshot dirty check an…
avinash-newrelic Mar 13, 2026
d661e32
docs: update DATAMODEL, advanced, and ONBOARDING for QoE aggregate in…
avinash-newrelic Mar 13, 2026
29d23c4
feat: register QoE provider at CONTENT_REQUEST and skip first buffer …
avinash-newrelic Mar 13, 2026
4fc89c2
docs: update QOE_AGGREGATE rebuffering description in DATAMODEL
avinash-newrelic Mar 13, 2026
9747785
feat: add qoeAggregateVersion attribute to QOE_AGGREGATE events
avinash-newrelic Mar 15, 2026
85603aa
feat: add playerName, playerVersion, numberOfErrors and instrumentati…
avinash-newrelic Mar 15, 2026
47b6b8e
fix: preserve totalPreRollAdTime across multiple pre-roll ad breaks
avinash-newrelic Mar 15, 2026
8918296
test: add QoE aggregator and harvest manager unit tests
avinash-newrelic Mar 15, 2026
f6db9a9
fix: address QoE aggregate review feedback
avinash-newrelic Mar 17, 2026
8dd64c2
fix: separate ad and content playtime tracking to prevent contamination
avinash-newrelic Mar 18, 2026
f82b24c
fix: prevent pre-roll ad time contamination in totalPlaytime
avinash-newrelic Mar 18, 2026
34b4535
fix: ad events should set totalAdPlaytime field, not totalPlaytime
avinash-newrelic Mar 18, 2026
625c25a
fix: correct startupTime calculation in QoE aggregator
avinash-newrelic Mar 18, 2026
5a5c02b
fix: comprehensive totalPlaytime and crash issues
avinash-newrelic Mar 18, 2026
1aa28f9
fix: deduplicate QOE_AGGREGATE events at harvest boundary conditions
avinash-newrelic Mar 19, 2026
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
16 changes: 16 additions & 0 deletions DATAMODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ An Attribute is a piece of data associated with an event. Attributes provide add
| CONTENT_BUFFER_END | Content video buffering ended. |
| CONTENT_HEARTBEAT | Content video heartbeat, an event that happens once every 30 seconds while the video is playing. |
| CONTENT_RENDITION_CHANGE | Content video stream quality changed. |
| QOE_AGGREGATE | Quality of Experience aggregate event. Sent on qualifying harvest cycles when KPI values have changed, and once at content end. Requires `withQoeAggregateEnabled:YES` in configuration. Frequency controlled by `withQoeAggregateIntervalMultiplier:`. |

#### QOE_AGGREGATE Attributes

QOE_AGGREGATE events carry all standard VideoAction attributes (viewId, viewSession, contentId, etc.) plus the following KPI attributes:

| Attribute Name | Definition |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| startupTime | Time from content request to content start, excluding pre-roll ad time (ms). |
| peakBitrate | Highest observed bitrate during playback (bps). |
| averageBitrate | Time-weighted average bitrate during active playback only — paused/buffered/seeking time is excluded (bps). |
| totalPlaytime | Total content playtime excluding pause, buffer, and seek (ms). Computed in real-time at harvest. |
| totalRebufferingTime | Total rebuffering time (ms). Counts all CONTENT_BUFFER_END events except the first one in the session (initial load). |
| rebufferingRatio | Rebuffering time as a percentage of total playtime: (totalRebufferingTime / totalPlaytime) * 100. |
| hadStartupError | True if an error occurred before content start. |
| hadPlaybackError | True if an error occurred after content start. |

### VideoAdAction

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ - (IBAction)clickBunnyVideo:(id)sender {
}

- (IBAction)clickSintelVideo:(id)sender {
[self playVideo:@"https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"];
[self playVideo:@"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8"];
}

- (IBAction)clickAirshowLive:(id)sender {
[self playVideo:@"http://cdn3.viblast.com/streams/hls/airshow/playlist.m3u8"];
[self playVideo:@"https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8"];
}

- (void)viewDidLoad {
Expand Down
18 changes: 10 additions & 8 deletions NRAVPlayerTracker/NRAVPlayerTracker/Tracker/NRTrackerAVPlayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ - (void)unregisterListeners {
@catch(id e) {}

self.isLive = NO;

self.lastRenditionHeight = 0;
self.lastRenditionWidth = 0;
self.lastTrackerTimeEvent = 0;
Expand Down Expand Up @@ -188,15 +188,17 @@ - (void)registerListeners {

self.timeObserver =
[self.playerInstance addPeriodicTimeObserverForInterval:CMTimeMake(1, 2) queue:NULL usingBlock:^(CMTime time) {

AV_LOG(@"(AVPlayerTracker) Time Observer = %f , rate = %f , duration = %f", CMTimeGetSeconds(time), self.playerInstance.rate, CMTimeGetSeconds(self.playerInstance.currentItem.duration));

// Check various state changes periodically
[self periodicVideoStateCheck];

// If duration is NaN, then is live streaming. Otherwise is VoD.
self.isLive = isnan(CMTimeGetSeconds(self.playerInstance.currentItem.duration));


// Only update live status after item metadata is loaded to prevent race condition
if (self.playerInstance.currentItem.status == AVPlayerItemStatusReadyToPlay) {
self.isLive = isnan(CMTimeGetSeconds(self.playerInstance.currentItem.duration));
}

if (self.playerInstance.rate > 0.0) {
[self sendStart];
[self sendBufferEnd];
Expand Down Expand Up @@ -249,7 +251,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
else if ([keyPath isEqualToString:@"currentItem.status"]) {
if (self.playerInstance.currentItem.status == AVPlayerItemStatusFailed) {
AV_LOG(@"(AVPlayerTracker) Error While Playing = %@", self.playerInstance.currentItem.error);

if (self.playerInstance.currentItem.error) {
[self sendError:self.playerInstance.currentItem.error];
}
Expand Down
32 changes: 28 additions & 4 deletions NewRelicVideoCore/NewRelicVideoCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
9CCH0002258CD4750044FAB0 /* NRChrono.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CCH0003258CD4750044FAB0 /* NRChrono.h */; };
9CCH0004258CD4750044FAB0 /* NRChrono.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CCH0005258CD4750044FAB0 /* NRChrono.m */; };
9CCH0006258CD4750044FAB0 /* NRChrono.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CCH0005258CD4750044FAB0 /* NRChrono.m */; };
9CQOE0001258CD4750044FAB0 /* NRQoEAggregator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CQOE0003258CD4750044FAB0 /* NRQoEAggregator.h */; };
9CQOE0002258CD4750044FAB0 /* NRQoEAggregator.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CQOE0003258CD4750044FAB0 /* NRQoEAggregator.h */; };
9CQOE0004258CD4750044FAB0 /* NRQoEAggregator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CQOE0005258CD4750044FAB0 /* NRQoEAggregator.m */; };
9CQOE0006258CD4750044FAB0 /* NRQoEAggregator.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CQOE0005258CD4750044FAB0 /* NRQoEAggregator.m */; };
9CNEW71CE35C656124EB2B8CA /* NRVAVideo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CNEW82553CBB1CF24833872D /* NRVAVideo.h */; settings = {ATTRIBUTES = (Public, ); }; };
9CNEW675C6E5EA3634CCC8FC6 /* NRVAVideo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CNEW82553CBB1CF24833872D /* NRVAVideo.h */; settings = {ATTRIBUTES = (Public, ); }; };
9CNEWE7DE82391DA345968344 /* NRVAVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CNEWD69ECFB4F99C4850A773 /* NRVAVideo.m */; };
Expand Down Expand Up @@ -145,6 +149,10 @@
9CE7992B25837C9400157199 /* NewRelicVideoCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CE7991D25837C9400157199 /* NewRelicVideoCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
9CE7994425837D0B00157199 /* NewRelicVideoCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CE7993B25837D0A00157199 /* NewRelicVideoCore.framework */; };
72BC6CA495AAC595919A0B84 /* NRTimeSinceTableThreadSafetyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B9B6605B2045E3744CF0325D /* NRTimeSinceTableThreadSafetyTests.m */; };
QOETEST0001000000000001 /* NRQoEAggregatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = QOETEST0001000000000003 /* NRQoEAggregatorTests.m */; };
QOETEST0001000000000002 /* NRQoEAggregatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = QOETEST0001000000000003 /* NRQoEAggregatorTests.m */; };
QOETEST0002000000000001 /* NRVAHarvestManagerQoETests.m in Sources */ = {isa = PBXBuildFile; fileRef = QOETEST0002000000000003 /* NRVAHarvestManagerQoETests.m */; };
QOETEST0002000000000002 /* NRVAHarvestManagerQoETests.m in Sources */ = {isa = PBXBuildFile; fileRef = QOETEST0002000000000003 /* NRVAHarvestManagerQoETests.m */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -203,6 +211,8 @@
9C4F8956258D07A60070CC2D /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
9CCH0003258CD4750044FAB0 /* NRChrono.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NRChrono.h; sourceTree = "<group>"; };
9CCH0005258CD4750044FAB0 /* NRChrono.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NRChrono.m; sourceTree = "<group>"; };
9CQOE0003258CD4750044FAB0 /* NRQoEAggregator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NRQoEAggregator.h; sourceTree = "<group>"; };
9CQOE0005258CD4750044FAB0 /* NRQoEAggregator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NRQoEAggregator.m; sourceTree = "<group>"; };
9CNEW82553CBB1CF24833872D /* NRVAVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NRVAVideo.h; sourceTree = "<group>"; };
9CNEWD69ECFB4F99C4850A773 /* NRVAVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NRVAVideo.m; sourceTree = "<group>"; };
9CNEW585E2C2BCA524422AABE /* NRVAVideoConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NRVAVideoConfiguration.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -256,6 +266,8 @@
9CE7993B25837D0A00157199 /* NewRelicVideoCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NewRelicVideoCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9CE7994325837D0B00157199 /* NewRelicVideoCore-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "NewRelicVideoCore-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
B9B6605B2045E3744CF0325D /* NRTimeSinceTableThreadSafetyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NRTimeSinceTableThreadSafetyTests.m; sourceTree = "<group>"; };
QOETEST0001000000000003 /* NRQoEAggregatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NRQoEAggregatorTests.m; sourceTree = "<group>"; };
QOETEST0002000000000003 /* NRVAHarvestManagerQoETests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NRVAHarvestManagerQoETests.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -415,6 +427,8 @@
9C01D736258CD4750044FAB0 /* NRTimeSinceTable.h */,
9CCH0003258CD4750044FAB0 /* NRChrono.h */,
9CCH0005258CD4750044FAB0 /* NRChrono.m */,
9CQOE0003258CD4750044FAB0 /* NRQoEAggregator.h */,
9CQOE0005258CD4750044FAB0 /* NRQoEAggregator.m */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -484,6 +498,8 @@
isa = PBXGroup;
children = (
B9B6605B2045E3744CF0325D /* NRTimeSinceTableThreadSafetyTests.m */,
QOETEST0001000000000003 /* NRQoEAggregatorTests.m */,
QOETEST0002000000000003 /* NRVAHarvestManagerQoETests.m */,
9CE7992825837C9400157199 /* NewRelicVideoCoreTests.m */,
9CE7992A25837C9400157199 /* Info.plist */,
);
Expand All @@ -509,6 +525,7 @@
9C01D741258CD4760044FAB0 /* NRVideoLog.h in Headers */,
9C01D75B258CD4780044FAB0 /* NewRelicVideoAgent.h in Headers */,
9CCH0001258CD4750044FAB0 /* NRChrono.h in Headers */,
9CQOE0001258CD4750044FAB0 /* NRQoEAggregator.h in Headers */,
9CNEW71CE35C656124EB2B8CA /* NRVAVideo.h in Headers */,
9CNEW12E01B93C3F0469BA1C2 /* NRVAVideoConfiguration.h in Headers */,
9CNEW5DA15487F5374398B9C0 /* NRVAVideoPlayerConfiguration.h in Headers */,
Expand Down Expand Up @@ -552,6 +569,7 @@
9C01D742258CD4760044FAB0 /* NRVideoLog.h in Headers */,
9C01D75C258CD4780044FAB0 /* NewRelicVideoAgent.h in Headers */,
9CCH0002258CD4750044FAB0 /* NRChrono.h in Headers */,
9CQOE0002258CD4750044FAB0 /* NRQoEAggregator.h in Headers */,
9CNEW675C6E5EA3634CCC8FC6 /* NRVAVideo.h in Headers */,
9CNEW727ADBB0BAB745EA9FA7 /* NRVAVideoConfiguration.h in Headers */,
9CNEW676BA960CA6441BE893A /* NRVAVideoPlayerConfiguration.h in Headers */,
Expand Down Expand Up @@ -746,6 +764,7 @@
9C01D73B258CD4750044FAB0 /* NRVideoTracker.m in Sources */,
9C01D753258CD4770044FAB0 /* NREventAttributes.m in Sources */,
9CCH0004258CD4750044FAB0 /* NRChrono.m in Sources */,
9CQOE0004258CD4750044FAB0 /* NRQoEAggregator.m in Sources */,
9CNEWE7DE82391DA345968344 /* NRVAVideo.m in Sources */,
9CNEW8D97F29E74D14E979C78 /* NRVAVideoConfiguration.m in Sources */,
9CNEW6165FB0E3C314395A50D /* NRVAVideoPlayerConfiguration.m in Sources */,
Expand All @@ -772,8 +791,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
72BC6CA495AAC595919A0B84 /* NRTimeSinceTableThreadSafetyTests.m in Sources */,
9CE7992925837C9400157199 /* NewRelicVideoCoreTests.m in Sources */,
72BC6CA495AAC595919A0B84 /* NRTimeSinceTableThreadSafetyTests.m in Sources */,
QOETEST0001000000000001 /* NRQoEAggregatorTests.m in Sources */,
QOETEST0002000000000001 /* NRVAHarvestManagerQoETests.m in Sources */,
9CE7992925837C9400157199 /* NewRelicVideoCoreTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -791,6 +812,7 @@
9C01D73C258CD4750044FAB0 /* NRVideoTracker.m in Sources */,
9C01D754258CD4770044FAB0 /* NREventAttributes.m in Sources */,
9CCH0006258CD4750044FAB0 /* NRChrono.m in Sources */,
9CQOE0006258CD4750044FAB0 /* NRQoEAggregator.m in Sources */,
9CNEWA32CB040EC9043BC8378 /* NRVAVideo.m in Sources */,
9CNEW6D58B157475C47B9B313 /* NRVAVideoConfiguration.m in Sources */,
9CNEW6DA1F57BE71348A2BF7A /* NRVAVideoPlayerConfiguration.m in Sources */,
Expand All @@ -817,8 +839,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
72BC6CA495AAC595919A0B84 /* NRTimeSinceTableThreadSafetyTests.m in Sources */,
);
72BC6CA495AAC595919A0B84 /* NRTimeSinceTableThreadSafetyTests.m in Sources */,
QOETEST0001000000000002 /* NRQoEAggregatorTests.m in Sources */,
QOETEST0002000000000002 /* NRVAHarvestManagerQoETests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
Expand Down
15 changes: 15 additions & 0 deletions NewRelicVideoCore/NewRelicVideoCore/Harvest/NRVAHarvestManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,20 @@
*/
- (NSString *)getRecoveryStatus;

/**
* Block that returns a fully-formed QoE event dict (with eventType, actionName, timestamp).
* Called by the harvest manager on qualifying harvest cycles when QoE attributes have changed.
* Set by the tracker at content start, cleared automatically after the final QoE is collected.
*/
@property (nonatomic, copy, nullable) NSDictionary * (^qoeEventProvider)(void);

/**
* Enqueue a pre-built final QoE event for delivery on the next harvest.
* The event is built eagerly at sendEnd time (while tracker state is still valid)
* and dispatched to the harvestQueue for safe, race-free pickup.
* Takes priority over the regular qoeEventProvider and auto-clears the provider.
* @param event Fully-formed QoE event dict (eventType, actionName, timestamp already set).
*/
- (void)enqueueFinalQoeEvent:(NSDictionary *)event;

@end
Loading
Loading