feat(NRMediaTailorTracker): new iOS / tvOS module for AWS MediaTailor SSAI - #217
feat(NRMediaTailorTracker): new iOS / tvOS module for AWS MediaTailor SSAI#217avinash-newrelic wants to merge 26 commits into
Conversation
Mirrors NRAVPlayerTracker's iOS-only framework layout. New target builds a Swift-based `NRTrackerMediaTailor` stub that extends `NRVideoTracker` from NewRelicVideoCore. Carries the 10 SDK-boundary anti-pattern guardrails from FEATURE_SPEC §5 in the file header so T02-T09 inherit the non-goals. - NRMediaTailorTracker.podspec at repo root, depends on NewRelicVideoAgent - NRMediaTailorTracker.xcodeproj with shared scheme, iOS 13.0 deployment - Swift 5.0, @objc-exposed class so the public symbol matches the spec - PrivacyInfo.xcprivacy mirrors NRAVPlayerTracker's
Repo convention is Obj-C, iOS 12, tvOS 12 across all sibling trackers
(NRIMATracker, NRAVPlayerTracker, parent NewRelicVideoAgent). Bell/DeltaTre
is a tvOS customer, so v1 must ship both targets. This pivot rewrites the
T01 scaffold from a86b1f7 to match.
- NRTrackerMediaTailor.swift → NRTrackerMediaTailor.{h,m} (ObjC stub)
- Single xcodeproj, two targets: NRMediaTailorTracker-iOS,
NRMediaTailorTracker-tvOS, each with a shared scheme
- Deployment targets lowered to iOS 12 / tvOS 12
- Podspec mirrors NRAVPlayerTracker.podspec: dual ios+tvos
deployment, .{h,m} source glob, NewRelicVideoAgent dependency
- 10-anti-pattern guardrail block carried verbatim from the Swift file
into NRTrackerMediaTailor.m
Add module README and CONTRIBUTING that document the 10 SDK-boundary anti-patterns from FEATURE_SPEC §5. The matching guardrails block at the top of NRTrackerMediaTailor.m was added during T01 and verified in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port the Android MTDetector class to Objective-C. Adds the NRMediaTailorTracker/Detection module with class methods to: - detect MediaTailor URLs (substring match on `mediatailor`) - extract sessionId from `aws.sessionId` or `sessionId` query params - derive the fallback `/v1/tracking/<sessionId>` URL by rewriting `/v1/master/` or `/v1/session/` paths Per Bug B4, this class is the FALLBACK; the PRIMARY tracking-URL discovery path parses HLS EXT-X-DATERANGE CLASS="tracking" / DASH EventStream markers and is implemented in T05. Header documents this clearly. Adds a new NRMediaTailorTrackerTests iOS XCTest target (and a shared scheme) with 18 unit tests covering HLS, DASH, both sessionId query-param forms, malformed URLs, nil URLs, and the default segment-marker list. Test target embeds NRMediaTailorTracker.framework and NewRelicVideoCore.framework into the .xctest bundle so `xcodebuild test` runs against the prebuilt video-core framework. Verified: - xcodebuild -scheme NRMediaTailorTracker-iOS ** BUILD SUCCEEDED ** - xcodebuild -scheme NRMediaTailorTracker-tvOS ** BUILD SUCCEEDED ** - xcodebuild test -scheme NRMediaTailorTrackerTests Executed 18 tests, with 0 failures ** TEST SUCCEEDED ** Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…MTAdBreak, MTAdPod) Adds the Obj-C data model layer for the MediaTailor tracking JSON. Immutable value objects for the wire shape (MTTrackingResponse / MTAvail / MTAd / MTTrackingEvent / MTNonLinearAvail); mutable runtime state for the schedule (MTAdBreak / MTAdPod with hasFired* dedup flags). Hand-rolled fromDictionary: factories (no Codable; Obj-C only). Public AD_ERROR vocabulary lives in MTAdErrorCode. Bugs fixed: - B1: MTTrackingResponse.nextToken decoded; empty string normalises to nil. - B2: MTAd.creativeId is the primary identity; -primaryKey returns creativeId or the <availId>:<adId> composite fallback. - B5: trackingEvents[*].startTimeInSeconds is RELATIVE TO AD START — stored as relativeToAdStartMs, with a header comment explaining the distinction vs. MTAvail/MTAd absolute timing. Documented per atomic facts §1, §9. - A2 surface: MTAvail.isNoFill computed from ads.count == 0. - A8 surface: MTAvail.hasStartTime captures whether startTimeInSeconds was actually present in the JSON, so the schedule merger (T06) can emit MISSING_AVAIL_START instead of silently inferring. Test target wiring: - 28 new unit tests across MTTrackingResponseDecodeTests + MTAdDecodeTests. - 5 JSON fixtures (full, empty_ads, with_nexttoken, missing_creativeId, live) bundled as test target resources. - Files added to both iOS and tvOS framework targets; tests run via the pre-existing NRMediaTailorTrackerTests scheme (iOS-only test target). Build + test verification: - xcodebuild -scheme NRMediaTailorTracker-iOS -sdk iphonesimulator → BUILD SUCCEEDED - xcodebuild -scheme NRMediaTailorTracker-tvOS -sdk appletvsimulator → BUILD SUCCEEDED - xcodebuild test -scheme NRMediaTailorTrackerTests -sdk iphonesimulator → 46 tests / 0 failures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements MTHlsParser + MTManifestParseResult to parse `.m3u8` manifests into ad breaks and (when present) recover the tracking URL directly from a manifest marker. Bug B4 — primary vs fallback discovery: - PRIMARY: parse `#EXT-X-DATERANGE` entries with `CLASS="com.apple.hls.interstitial"` or `CLASS="tracking"` and extract their `URI` attribute. Atomic-facts §4 Method 1. - FALLBACK: callers use `+[MTDetector deriveTrackingURL:]` (URL-rewrite) when no DATERANGE marker is present. Break / pod detection: - Per-segment URL marker match against MTDetector.defaultSegmentMarkers (`segments.mediatailor`, `/v1/hlssegment/`, `/v1/dashsegment/`, `/tm/`) plus caller-supplied `customSegmentMarkers`. - Pod boundaries inside a break via `#EXT-X-DISCONTINUITY`. - `#EXT-X-PROGRAM-DATE-TIME` immediately before the first ad segment populates `MTAdBreak.availProgramDateTime` (Bug A4 / A8 live identity). Filters & guards: - 500 ms min ad duration (matches Android `MIN_AD_DURATION_MS`). - Bug A6: pods that would overshoot the parent break are clamped to the break boundary; clamp count surfaced via `+[MTHlsParser lastClampedPodCount]`. Clamp helper exposed as `+clampPodIfNeeded:toBreak:` for the schedule merger (T06) to reuse. 15 unit tests across 5 fixtures cover: - VOD with DATERANGE → trackingURL + 1 break / 1 pod - DATERANGE `CLASS="tracking"` variant + relative URI resolution - DATERANGE non-tracking class ignored - VOD segment-markers only → trackingURL nil + 1 break / 2 pods - Custom segment marker override picks up non-default CDN paths - Live sliding window → availProgramDateTime propagated - Empty avail → trackingURL recovered, breaks empty - Sub-500ms ad segment dropped - Multi-pod break invariant: every pod end ≤ break end (Bug A6) - Direct clamp helper: overshooting pod clamped, in-range untouched - Nil/empty manifest, content-only manifest → empty result Build/test verification: - xcodebuild -scheme NRMediaTailorTracker-iOS ** BUILD SUCCEEDED ** - xcodebuild -scheme NRMediaTailorTracker-tvOS ** BUILD SUCCEEDED ** - xcodebuild test -scheme NRMediaTailorTrackerTests Executed 61 tests, with 0 failures (15 new MTHlsParserTests) ** TEST SUCCEEDED ** Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`MTTrackingClient` fetches the MediaTailor /v1/tracking endpoint and addresses the three Android deviations from the documented contract: - B1 (FIXED): nextToken is round-tripped per atomic facts f-track-client-113..f-track-client-121. First call sends no token; the server's nextToken is captured and echoed as a query param on every subsequent call. HTTP 400 drops the token and retries once with none. - A1 (DROPPED): no `?t=<wall-clock>` cache-bust query param. The Android code's cache-busting defeats the backend's session-stable cache; we rely on the documented pagination contract instead. - B3 (PARITY): HTTP GET. Atomic fact f-track-client-119 documents POST but Android uses GET and the user has chosen to retain Android parity — code comment cites the deviation so a future contributor doesn't silently "fix" it. Implementation notes: - NSObject + serial dispatch queue (Obj-C analogue of a Swift actor) - Block-based completion delivered on the main queue - NSError domain `MTTrackingErrorDomain` with codes for Timeout/NetworkFailure/TokenExpired/InvalidResponse/ParseFailed/Cancelled - 5s timeout, one retry on transient network errors - `-cancel` and `-resetSession` for player teardown Tests cover the full B1 lifecycle: first/second-call token semantics, HTTP-400 expiry + auto-retry, timeout mapping, mid-flight cancel, same-token returned, malformed JSON, A1 strip, B3 GET, resetSession.
Add MTManifestParser Obj-C protocol so the tracker can swap manifest
parsers without forking the SDK. MTHlsParser conforms; a stub
MTDashParser ships as a placeholder that returns an empty result and
logs a warning so misconfigured customers notice immediately. Inject
via -[NRTrackerMediaTailor setManifestParser:].
The protocol returns a non-null MTManifestParseResult (deviation from
the task description's "return nil") — the protocol declares non-null
and tests assert non-nil, which is the safer contract.
Per locked decision (FEATURE_SPEC §8): HLS-only first release, DASH
adapter ships as a fast-follow.
Files:
- Parser/MTManifestParser.h (protocol)
- Parser/MTDashParser.{h,m} (stub)
- Parser/MTHlsParser.{h,m} (declare + implement <MTManifestParser>)
- Tracker/NRTrackerMediaTailor.{h,m} (setManifestParser:)
- NRMediaTailorTrackerTests/Parser/MTManifestParserSeamTests.m
- README.md (DASH adapter seam section)
- NRMediaTailorTracker.xcodeproj/project.pbxproj (register new files
on iOS + tvOS targets, fresh FA100/FA200 UUID prefixes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the HLS manifest carries a DATERANGE entry indicating an ad break
(CLASS="com.apple.hls.interstitial" or "tracking") but no ad-segment
markers follow, the parser now synthesizes a placeholder MTAdBreak with
isNoFill=YES and pods.count==0. This is the contract the T06 merger
consumes and matches atomic facts §6 ("Ad-server failure: tracker emits
AD_BREAK_START -> AD_BREAK_END immediately, no AD_START").
Before this fix, an empty-avail DATERANGE was lost — the parser only
emitted breaks when it saw actual ad-segment URLs. That meant manifests
where MediaTailor signaled an ad break but the ad server returned no
ads (and the tracking API was unavailable or silent) produced no
AD_BREAK_START/AD_BREAK_END events. The fix lets the manifest path
alone surface ad-server-failure avails to the state machine.
Originally implemented by ios-builder-2; committed by team-lead after
recovering the working-tree changes that were stranded during the
T11 / T04 / T05.1 three-way pbxproj merge dance.
Also includes minor pbxproj cleanup: removes 3 duplicate PBXBuildFile
and PBXFileReference entries that snuck in during the merge.
The T11 commit shipped -setManifestParser: but left the getter returning nil when never set. Convert to a property with a lazy getter that instantiates a shared MTHlsParser on first access. Callers that never inject a custom parser get HLS support for free; callers that inject a DASH adapter override before the first parse and keep it. Two new tests pin the contract: - testTracker_manifestParser_defaultsLazilyToMTHlsParser - testTracker_manifestParser_setterOverridesDefault Build + test final lines: - iOS : ** BUILD SUCCEEDED ** - tvOS : ** BUILD SUCCEEDED ** - Tests : ** TEST SUCCEEDED ** (82 tests / 0 failures; was 80) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stateless merger that combines manifest-parsed ad breaks (T05) with
tracking-API enrichment (T03/T04). Returns a MergedSchedule containing
the de-duplicated break list plus a queue of pending AD_ERROR events
that the state machine (T07/T08) will drain.
Bug fixes carried by this stage:
A2 — Empty avails are now no-fill breaks. avail.ads.count == 0 marks
the paired break with isNoFill = YES and queues
MTAdErrorCodeNoFill. The state machine will suppress AD_START /
quartiles and emit AD_BREAK_START → AD_ERROR(NO_FILL) → AD_BREAK_END.
Empty avails with no manifest counterpart are also synthesised so
customers see the no-fill in NRDB even when the player hasn't
reached the slate segments.
A3 — Manifest pod count != tracking ad count keeps manifest geometry.
Each pod is enriched with metadata from the closest-by-startTime
tracking ad. podCountMismatch = YES is flagged on the break and
MTAdErrorCodeManifestTrackingMismatch is queued. The merger no
longer wipes pod boundaries.
A4 — Compound de-dup key (availId, adProgramDateTime ?? startTimeMs).
VOD collapses to startTimeMs; live uses wall-clock so HLS sliding-
window rotation no longer produces duplicate breaks.
A8 — Missing avail startTimeInSeconds logs a dataIntegrityWarning,
queues MTAdErrorCodeMissingAvailStart, and falls back to the
first ad's startTime — instead of Android's silent inference.
B2 — Pod identity uses [MTAd primaryKey] (creativeId primary,
<availId>:<adId> composite fallback).
10 new tests cover the golden path, all four Group A bug fixes, B2
identity in both presence and absence of creativeId, and a nil-input
edge case.
Build + test final lines:
- iOS : ** BUILD SUCCEEDED **
- tvOS : ** BUILD SUCCEEDED **
- Tests : ** TEST SUCCEEDED ** (92 tests / 0 failures; was 82)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rgedSchedule Adds two derived counters on MergedSchedule: - podCountMismatchCount — number of breaks with podCountMismatch == YES - dataIntegrityWarningCount — number of MISSING_AVAIL_START pending errors Both are computed in -initWithBreaks:pendingErrors: by walking the input arrays once, so callers don't have to re-scan. Exposed for the state machine (T07) and event emitter (T08) telemetry without forcing them to re-derive. Three new tests: - testManifestOnly_nilTracking_breaksPassThroughUnchanged (manifest-only flow) - testCounters_podCountMismatchCount_aggregatesAcrossBreaks (counter sanity) - testCounters_dataIntegrityWarningCount_countsMissingAvailStart Build + test final lines: - iOS : ** BUILD SUCCEEDED ** - tvOS : ** BUILD SUCCEEDED ** - Tests : ** TEST SUCCEEDED ** (95 tests / 0 failures; was 92) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MTPlayheadStateMachine consumes a MergedSchedule (T06) and an AVPlayer, and drives an explicit IN_CONTENT → IN_BREAK → IN_POD → quartile flow that the event emitter (T08) will wire to NRVideoTracker.sendXxx calls. The state machine exposes a -tickAtPositionMs: test seam, so tests run without a real AVPlayer or wall-clock dependency. The AVPlayer integration is a thin AVPlayer.addPeriodicTimeObserverForInterval wrapper around the same tick method. Bug fixes: - A5: playheadPollInterval is configurable on init (default 0.250s; non-positive inputs clamp to default). - A6 (state-machine side): a pod ending before its break ends transits back to IN_BREAK idle and waits for break.endTimeMs; no spurious events. Parser-side clamp policy is unchanged (see memory entry [[a6-clamp-vs-reject]]). Error drain ordering matches the atomic-facts contract: - AD_BREAK_START → AD_ERROR(NO_FILL) → AD_BREAK_END for no-fill breaks - AD_BREAK_START → AD_ERROR(MISSING_AVAIL_START) → AD_START for the A8 case Errors drain from MergedSchedule.pendingErrors using pointer identity, so each error fires exactly once even across backward seeks. Dedup uses the existing MTAdBreak/MTAdPod hasFired* flags from T03 — backward seeks never re-fire a quartile, POD_START, or BREAK_START. Tests (8 total, all clock-free via -tickAtPositionMs:): - Golden: 1 break + 2 pods → full event sequence in order - No-fill: BREAK_START + ERROR(NO_FILL) + BREAK_END, no pod/quartile - Backward seek inside pod: no duplicate quartiles - Skip past entire break: no events (matches IMA tracker behaviour) - A5: configurable poll interval honored; 0.0 and -1.0 clamp to 0.250 - A8 drain order: BREAK_START → ERROR(MISSING_AVAIL_START) → POD_START - A6: last pod ends before break end → idle in IN_BREAK then BREAK_END - Forward seek across multiple quartiles: all three fire in one tick xcodebuild gates: - NRMediaTailorTracker-iOS iphonesimulator → BUILD SUCCEEDED - NRMediaTailorTracker-tvOS appletvsimulator → BUILD SUCCEEDED - NRMediaTailorTrackerTests iPhone 16 → 103 tests / 0 failures Pbxproj UUID prefix: C70 (source) / C71 (tests). No collisions with 9CA0, AB10, CD1, CD2, CE3, EE1, EE2, FA1, FA2, DB1, DB2. Originally assigned to ios-builder-1; they shipped MTAdState.h then stalled. Team-lead extracted MTAdState.h from their tree and wrote the remainder.
…attrs) NRTrackerMediaTailor now adopts MTPlayheadStateMachineDelegate. Each state transition translates to the corresponding NRVideoTracker send call: enteredBreak: → sendAdBreakStart enteredPod: → sendRequest + sendStart crossedQuartile: → sendAdQuartile (currentQuartileNumber drives getAdQuartile) exitedPod: → sendEnd exitedBreak: → sendAdBreakEnd raisedError: → sendVideoErrorEvent:AD_ERROR with errorCode attr Public lifecycle (T09 will extend further): - startTrackingWithSchedule: builds the state machine + subscribes - stopTracking: tears down (idempotent) - notifyAdSkipped: app-invoked, fires AD_SKIP if inside a pod Bug fixes: - B6: AD_ERROR is wired for all seven MTAdErrorCode values. Each event carries `errorCode` (canonical string) and `errorMessage` attributes. - B7: getAttributes: always emits `availProgramDateTime` and `adProgramDateTime` keys, even when the source values are nil (empty string), so live-stream consumers can reliably correlate. NRVideoTracker overrides: - getIsAd → @1 - getAdBreakId → current break's availId - getAdCreativeId → current pod's creativeId - getAdQuartile → currentQuartileNumber (set by the delegate path) - getAdPartner → current pod's adSystem - getTrackerName → "NRMediaTailorTracker" - getAttributes:attributes: merges in every pod / break attribute the spec lists (availId, adId, creativeId, adTitle, adSystem, creativeSequence, vastAdId, skipOffset, isBumper, noFill, podCountMismatch, plus the always-emitted programDateTime keys). Tests (8 new, total now 111): - Golden path: full sequence in order - No-fill: BREAK_START + AD_ERROR(NO_FILL) + BREAK_END, no pod events - B6: all seven error codes round-trip through AD_ERROR - B7: programDateTime keys present in every emitted attribute dict - Pod attributes land on AD_START - notifyAdSkipped fires AD_SKIP when inside a pod - notifyAdSkipped is a no-op outside a pod - stopTracking is idempotent xcodebuild gates: - NRMediaTailorTracker-iOS iphonesimulator → BUILD SUCCEEDED - NRMediaTailorTracker-tvOS appletvsimulator → BUILD SUCCEEDED - NRMediaTailorTrackerTests iPhone 16 → 111 tests / 0 failures Pbxproj UUID prefix: D80 (test file only — no new source files).
Adds full player attachment + teardown: setPlayer: - Accepts an AVPlayer; non-AVPlayer args fall through to super. - Detaches the previous player (state-machine time observer + KVO) before installing the new one. - Registers KVO on `timeControlStatus` with a static context pointer; the observer emits AD_PAUSE / AD_RESUME only when the tracker is currently inside an ad break (main-content pause/resume is the customer's content tracker's concern). dispose: - Idempotent — early-return on second call. - Cancels any in-flight `MTTrackingClient` request and resets the client's session (drops the nextToken). - Tears down the state machine via `stopTracking`. - Removes the KVO observer and clears the AVPlayer reference. - Calls super.dispose so the base NRTracker chain runs. - After dispose, ALL delegate callbacks (incl. raisedError:) and notifyAdSkipped / setPlayer: / startTrackingWithSchedule: early-return. The tracker is single-use. dealloc: - Calls dispose defensively, so customers who simply drop their strong reference get a clean teardown without leaking the AVPlayer KVO registration. Tests (8 new, total now 119): - dispose is idempotent - After dispose, further work is a no-op (no crashes, state cleared) - setPlayer attaches the state machine when tracking is active - Re-attaching swaps players cleanly (no crash, dispose clean) - setPlayer with non-AVPlayer args delegates to super - setPlayer after dispose is a no-op - Memory leak test: tracker releases cleanly after strong refs drop - stopTracking leaves the player attached (vs dispose which detaches) xcodebuild gates: - NRMediaTailorTracker-iOS iphonesimulator → BUILD SUCCEEDED - NRMediaTailorTracker-tvOS appletvsimulator → BUILD SUCCEEDED - NRMediaTailorTrackerTests iPhone 16 → 119 tests / 0 failures Pbxproj UUID prefix: D80x...A001 (re-use of T08's D80 namespace — no collision since A000 was the events test file and A001 is the lifecycle test file).
Wires NRTrackerMediaTailor into the existing sample app:
- Podfile: add `pod 'NRMediaTailorTracker', :path => '../../../'`.
- New `MediaTailorSamples.{h,m}` — placeholder sample-URL holder. The
default URL is intentionally a `REPLACE_ME` AWS endpoint; the
integration smoke path requires the operator to substitute their own
MediaTailor session URL before running.
- `ViewController.m`: new `-clickMediaTailorSample:` IBAction that builds
an AVPlayer, attaches a `NRTrackerMediaTailor`, and registers a
content tracker alongside via `NRVAVideo.addPlayer` (parity with the
IMA example flow). `dealloc` calls `-dispose` on the tracker for clean
teardown.
The IBAction is exposed for storyboard wiring; the actual button hookup
is intentionally not committed because changing the storyboard would
fork the existing example's UI without context. Drop a button in IB and
wire its `Touch Up Inside` event to `clickMediaTailorSample:` when
running the smoke test.
Verification flow (per FEATURE_SPEC §9 "Definition of Done"):
1. Replace `MediaTailorSamples.defaultSampleURLString` with a real
MediaTailor session URL.
2. `pod install`, build, run.
3. Capture proxy log (Charles / mitmproxy) and verify the
`/v1/tracking/<sessionId>` calls round-trip `nextToken` between
consecutive requests (proves Bug B1 fix end-to-end).
4. Confirm in NRDB that AD_BREAK_START → AD_START → 3×AD_QUARTILE →
AD_END → AD_BREAK_END fires for at least one ad break.
5. Optional: run the Android module side-by-side on the same stream;
event sequences should match.
In production the host app fetches the personalized manifest, parses
it via `MTHlsParser`, polls `/v1/tracking/<sessionId>` via
`MTTrackingClient`, merges with `MTAdScheduleMerger`, and feeds the
schedule into `-[NRTrackerMediaTailor startTrackingWithSchedule:]`.
The example's smoke path leaves the schedule-feeding step to the
operator since it depends on the operator's networking layer; see
`NRMediaTailorTracker/README.md` "Integration" for the full snippet.
- NRMediaTailorTracker/README.md: refresh the integration snippet to match the actual public API shipped across T01–T11 (init → setPlayer → startTrackingWithSchedule → dispose, with the MTHlsParser / MTTrackingClient / MTAdScheduleMerger sequence shown explicitly). The prior snippet referenced an `initWithPlayer:` initializer that we never shipped. Add a "Verification" section with the canonical xcodebuild commands for unit tests + coverage + both-platform builds + the integration smoke flow. - README.md (repo root): add NRMediaTailorTracker to the Modules table. Bump "three modules" to "four modules" in the section intro. - CHANGELOG.md: add an Unreleased entry covering the new module, the bug-fix list vs the Android reference (14 of 15 fixed; A7 deferred behind the DASH adapter seam), and the bumpfree wire compatibility with the existing event vocabulary. T12 coverage gate (≥70% line coverage on NRMediaTailorTracker.framework): Measured 89.92% line coverage via `xcrun xccov view --report --only-targets`. 119 tests across the 17 source files. Documented in the module README's new Verification section so any contributor can reproduce the measurement. A CI workflow that runs the test scheme on every PR is intentionally not added here — the repo's existing workflows are publish/release flows, and adding a PR-test workflow is a separate piece of CI engineering (runner configuration, simulator selection, cert handling) that doesn't belong with the docs sweep.
… variants scripts/bootstrap-newrelic-video-core.sh builds NewRelicVideoCore for iphonesimulator + iphoneos + appletvsimulator + appletvos under Debug, writing the framework artifacts to NewRelicVideoCore/build/Debug-<sdk>/NewRelicVideoCore.framework. That's the path every consumer module's xcodeproj declares in its framework search paths. Background: NRMediaTailorTracker (and existing siblings NRAVPlayerTracker, NRIMATracker) link against a pre-built NewRelicVideoCore framework rather than a source dependency. The build/ directory is gitignored, so on a fresh clone the tvOS scheme fails to link until someone builds NewRelicVideoCore tvOS first. Surfaced by ios-builder-1 during T01. Verified locally — all four build invocations end in ** BUILD SUCCEEDED ** on this machine. Added a "First-clone bootstrap" section to NRMediaTailorTracker/README.md telling new contributors to run the script after cloning. Re-run when bumping the NewRelicVideoCore source.
…button The MediaTailor smoke-test wiring originally landed in T13 (commit 1e7fd11) under Examples/iOS/SimplePlayerUsingPods. That was the wrong example — SimplePlayerUsingPods is the content-only playback demo. NRMediaTailorTracker is an ad-tracking module, so the example belongs in Examples/iOS/SimplePlayerWithAds alongside the existing IMA integration. Also: the T13 commit had latent bugs that prevented the example from building (Podfile.lock not updated; MediaTailorSamples.{h,m} created but never wired into the pbxproj). This commit ships the re-homed integration as a working example. Changes in SimplePlayerUsingPods (revert to pre-T13): - Podfile: drop the NRMediaTailorTracker pod entry - ViewController.m: revert to the pre-T13 content-only version - Delete MediaTailorSamples.{h,m} Changes in SimplePlayerWithAds (new home for the MediaTailor demo): - Podfile: add `pod 'NRMediaTailorTracker', :path => '../../../'` alongside the existing NRAVPlayerTracker / NRIMATracker pods. - New `MediaTailorSamples.{h,m}` — resolves the session URL from NSUserDefaults (`MediaTailorSampleURL` key) first, then the `MT_SAMPLE_URL` env var, then a `REPLACE_ME` placeholder. Lets the smoke-test operator plug in their real URL without editing source: defaults write com.newrelic.SimplePlayerWithAds \ MediaTailorSampleURL "https://<your.mediatailor.session.url>" - ViewController.m: new `clickMediaTailorSample:` IBAction and `playMediaTailorVideo:` internal method. Unlike the IMA buttons, this path skips IMAAdsLoader / IMAAdsManager — MediaTailor ads are stitched into the HLS manifest server-side, so no client-side ads SDK is needed. Instantiates NRTrackerMediaTailor, attaches the player, and registers a content tracker via NRVAVideo.addPlayer. Dealloc and viewDidAppear (dismiss path) call -dispose so the KVO/observer teardown runs cleanly. - Main.storyboard: new "MediaTailor Sample" button, wired to `clickMediaTailorSample:`, anchored 8pt below the Airshow button with centerX alignment. - SimplePlayerWithAds.xcodeproj/project.pbxproj: file references and build-file entries for MediaTailorSamples.{h,m} (UUID prefix MT0100xx — clearly distinct from the existing 9CEE25xx range). - Podfile.lock: regenerated by `pod install` — includes NRMediaTailorTracker 4.2.0. Verified locally: - `xcodebuild -workspace SimplePlayerWithAds.xcworkspace -scheme SimplePlayerWithAds -sdk iphonesimulator build` → BUILD SUCCEEDED. - The new button renders below "Airshow" and wires to the new IBAction (verified via storyboard XML diff). Verification flow (unchanged from T13 brief): 1. Replace `MediaTailorSamples.defaultSampleURLString` placeholder with a real MediaTailor session URL — either edit the placeholder in source, set the NSUserDefaults override, or set MT_SAMPLE_URL in the simulator's env. 2. Run the example app, tap "MediaTailor Sample". 3. Capture a proxy log and confirm `/v1/tracking/<sessionId>` calls round-trip `nextToken` between consecutive requests. 4. Confirm in NRDB that AD_BREAK_START → AD_START → 3×AD_QUARTILE → AD_END → AD_BREAK_END fires for at least one ad break.
Replaces the T11 stub `MTDashParser` with a full NSXMLParser-backed DASH MPD parser. Conforms to the `MTManifestParser` seam alongside `MTHlsParser`, so DASH now ships out of the box — no more "HLS only in v1". The headline fix is Bug A7. The Android `MTDashParser.java` classified a `<Period>` as ad based on the first `<Representation>`'s BaseURL. DASH periods can legitimately mix representations with different BaseURLs, so that's wrong. The iOS parser walks every `<Representation>` across every `<AdaptationSet>`, resolves the effective URL through the period → adaptation-set → representation `BaseURL` inheritance chain, and only classifies a period as ad when **all** representations match an ad-segment marker. Mixed → content. The mixed-period count is exposed via `-[MTDashParser mixedPeriodCount]` for tests + runtime telemetry. Also handles: - Multi-period VOD (period start from `start` attribute or running sum of prior durations; ISO 8601 PT… durations). - Dynamic live: `availabilityStartTime` + period start offset → `availProgramDateTime` on every break (Bug A4 live-identity contract). - SCTE-35 + `urn:aws:elemental:mediatailor:tracking` `<EventStream>` — takes precedence over BaseURL classification per atomic facts §10. - `<Location>` element → tracking URL (DASH analogue of HLS DATERANGE `CLASS="tracking"` URI). - Min ad duration 500 ms (matches HLS parser). - Graceful return-empty on nil / empty / non-UTF-8 / malformed XML / no-periods / period-missing-duration inputs. Fixtures + tests - `mediatailor_dash_vod_multiperiod.mpd` — content/ad/content, asserts single break at the middle period. - `mediatailor_dash_dynamic_live.mpd` — SCTE-35 event, asserts `availProgramDateTime` is populated from `availabilityStartTime`. - `mediatailor_dash_mixed_representations.mpd` — THE A7 fixture: 3 representations, 2 match ad marker, 1 doesn't → 0 breaks, `mixedPeriodCount == 1`. - `mediatailor_dash_no_periods.mpd` — degenerate `<MPD/>`, returns empty. Test count: 119 → 132 (13 new DASH tests). Both schemes build clean (`NRMediaTailorTracker-iOS` / `-tvOS`); all tests pass on the iPhone 16 simulator. Out of scope: model classes, merger, state machine — untouched per the T16 brief. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…he canonical contributor path
The script (scripts/bootstrap-newrelic-video-core.sh, added in T20) pre-built
NewRelicVideoCore for all four SDK/simulator slices to satisfy the standalone
xcodebuild path declared in NRMediaTailorTracker.xcodeproj's framework search
paths. That standalone path is a contributor convenience, not the canonical
workflow.
The canonical contributor workflow is the same one already used for sibling
modules: cd into the example app and pod install. CocoaPods resolves
NewRelicVideoCore (published as the NewRelicVideoAgent pod, module name
NewRelicVideoCore) and the dependent trackers via the local podspecs at
:path => '../../../'. NRMediaTailorTracker.podspec already declares
s.dependency 'NewRelicVideoAgent' so it slots in cleanly.
This commit:
• Deletes scripts/bootstrap-newrelic-video-core.sh (and the now-empty
scripts/ directory)
• Rewrites NRMediaTailorTracker/README.md "Verification" section to point
contributors at Examples/iOS/SimplePlayerWithAds + pod install +
SimplePlayerWithAds.xcworkspace. Standalone xcodebuild instructions
removed (they depended on the deleted script's output).
• Updates the test count from the stale 119 to the actual 132.
Sibling tracker READMEs (NRAVPlayerTracker, NRIMATracker) do not exist, so
there's no precedent to mirror; the minimal pods pointer matches the
SimplePlayerWithAds.Podfile that's already in the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Appends NRMediaTailorTracker to the list of frameworks the release script builds into universal XCFrameworks. Mirrors the NRAVPlayerTracker line: both schemes (iOS + tvOS), depends on NewRelicVideoCore, no extra setup. The trailing zip + GitHub Release upload steps use glob (mv *.xcframework xcframeworks/) so they pick up the new artifact transparently — no further edits needed. Without this, ios-publish.yml's "Build XCFrameworks" step would continue to omit NRMediaTailorTracker.xcframework from the released xcframeworks.zip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… validate + publish lists
Adds NRMediaTailorTracker to every pod-enumeration in the publish workflow:
• validate_with_retry list (line 129) — pod lib lint must pass before
publish-time
• publish_pod list (line 286) — pushed to CocoaPods trunk after the
NewRelicVideoAgent indexing wait, parallel to NRAVPlayerTracker /
NRIMATracker
• GitHub Release notes body (line 332)
• Dry-run summary echo (line 360)
• verify_pod post-publish check (line 387)
• "Users can now install with" echo (line 397)
Without these, every release would silently skip NRMediaTailorTracker —
the version bump would land in master, but the pod would not exist on
CocoaPods trunk at the new version.
Dependency order: NRMediaTailorTracker is registered as depending on
NewRelicVideoAgent (s.dependency in its podspec), so it must publish after
NewRelicVideoAgent indexing completes. The existing wait_for_indexing
gate on NewRelicVideoAgent at line 280 covers this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The base NRVideoTracker.getTrackerVersion returns (NSString *)[NSNull null].
Without a per-module override, every event the tracker emits would carry
trackerVersion = NSNull in NRDB — making it impossible to filter telemetry
by tracker version, correlate against module-specific bugs, or attribute
issues to a specific NRMediaTailorTracker release.
Sibling trackers (NRTrackerIMA, NRTrackerAVPlayer) each declare this
override returning the current module version. NRTrackerMediaTailor was
the only public tracker class in the repo missing it — a gap in the T08
event-emission parity work that didn't surface until we went to integrate
this module into ios-release.yml's version-bump workflow, which sed-patches
the hardcoded string in each module's tracker .m alongside the podspec.
Adds:
- (NSString *)getTrackerVersion {
return @"4.2.0";
}
placed after getTrackerName in the existing "NRVideoTracker attribute
overrides" pragma section. Matches NRTrackerIMA.m:106-108 verbatim apart
from the indentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… version-bump list The release workflow sed-replaces version strings in 3 places per module: (a) the podspec's s.version, (b) the tracker's hardcoded `return @"X.Y.Z";` in -getTrackerVersion, and (c) a verify-grep that prints the bumped value. Plus the source file lands in the workflow's `git add` list so the version-bump PR commit includes it. This commit wires NRMediaTailorTracker into all five touch points: - sed: NRMediaTailorTracker.podspec → s.version - sed: NRTrackerMediaTailor.m → -getTrackerVersion return literal - verify-grep for the podspec - verify-grep for the .m - `git add` for the .m Requires the previous commit (the -getTrackerVersion override returning @"4.2.0") — without that override the source-file sed would have nothing to match. After this, the next time the release workflow runs, NRMediaTailorTracker gets version-bumped alongside the existing three modules.
8983df7 to
82f201e
Compare
NRMediaTailorTracker (P0-110 / P0-111 / P0-112): - adSegmentPrefix: public custom-CDN ad-segment marker, plumbed to a new MTHlsParser.customSegmentMarkers instance property used on the MTManifestParser protocol path (nil/empty preserves default AWS markers). - trackingUrl: public override + -resolvedTrackingURLForManifestURL: (verbatim when set, else MTDetector deriveTrackingURL: fallback). - pollIntervalMs: public playhead poll cadence, default 250ms, clamped 100..5000ms with a warning; replaces the hard-coded 0.250 at -startTrackingWithSchedule:. NewRelicVideoCore — NRAdConfig (P0-116): - New NRAdConfig (+csai / +mediaTailor / +mediaTailorWithSegmentPrefix:… ) + adConfig on NRVAVideoPlayerConfiguration. addPlayer: now selects the ad-tracker class by type (NRTrackerIMA vs NRTrackerMediaTailor) via dynamic class loading and wires MediaTailor's player + adSegmentPrefix + trackingUrl. Legacy adEnabled:YES maps to csai (no behavior change). - SimplePlayerWithAds migrated to NRAdConfig.mediaTailor(). Tests: 8 core tests (NRAdConfig factories, legacy->csai mapping, config selection) + tracker config/clamp + custom-marker detection tests. Core and tracker suites and the sample workspace build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (_currentPeriod == nil) return; | ||
| double presentationTime = [attrs[@"presentationTime"] doubleValue]; | ||
| double duration = [attrs[@"duration"] doubleValue]; | ||
| NSTimeInterval startMs = (presentationTime / _eventStreamTimescale) * 1000.0; |
There was a problem hiding this comment.
SCTE-35 EventStream presentationTime isn't offset by the enclosing <Period>'s start. Per the DASH spec, EventStream Event presentationTime is relative to the containing Period's start, but this computes startMs directly from presentationTime with no addition of _currentPeriod.startMs. Contrast with finalizeCurrentPeriod (the non-EventStream branch), which correctly does p.hasStart ? p.startMs : _runningPriorPeriodEndMs. For any MPD with a <Period start="PT100S"> containing an EventStream ad marker, the resulting MTAdBreak.startTimeMs will be wrong by the Period's start offset, causing MTPlayheadStateMachine to fire AD_BREAK_START/quartiles at the wrong playhead position. The only EventStream fixture (mediatailor_dash_dynamic_live.mpd) uses start="PT0S", which masks this.
| self.avPlayer = nil; | ||
| } | ||
|
|
||
| - (void)observeValueForKeyPath:(NSString *)keyPath |
There was a problem hiding this comment.
timeControlStatus KVO handler reads currentBreak/currentPod without any thread/queue guarantee, while MTPlayheadStateMachine's delegate callbacks write those same properties on the main queue. AVFoundation doesn't guarantee timeControlStatus KVO notifications land on the main thread, so this can run concurrently with the state machine's main-queue mutations. To be precise about the actual risk: since currentBreak/currentPod are declared weak, ARC's weak-accessor machinery is itself thread-safe (no memory corruption), so this isn't a crash risk — but it is a real TOCTOU-style logical race: sendPause/sendResume can fire based on a break/pod that's concurrently exiting on the main queue, producing an ad-pause event for an ad that's already ended from the state machine's point of view. Worth hopping this handler onto the main queue before touching currentBreak/currentPod, to match the stated "main-queue only" contract documented elsewhere in this class.
| MTAvail *match = [self matchAvailForBreak:manifestBreak in:avails consumed:consumedAvails]; | ||
| if (match) { | ||
| [consumedAvails addObject:match]; | ||
| [self enrichBreak:manifestBreak withAvail:match outErrors:errors]; |
There was a problem hiding this comment.
enrichBreak: runs (and can queue an MTMergedScheduleError) before the dedup check on line 41-46 decides whether to drop this break. MTMergedScheduleError.adBreak is declared weak, and MergedSchedule.breaks is the only strong owner of MTAdBreak instances in the default (HLS) parsing path. If this break turns out to be a dedup duplicate (same key as an earlier break in this same call — the A4 "live window slide" case), it's enriched — possibly queuing an error — and then dropped from outBreaks at line 46. Once mergeManifestBreaks: returns, nothing retains that dropped break, so the queued error's weak adBreak becomes nil. MTPlayheadStateMachine.drainPendingErrorsForBreak: matches via err.adBreak != brk, which can never match a nil adBreak, so the queued error (e.g. a MISSING_AVAIL_START warning) is silently swallowed. Suggest either skipping enrichment for a break you already know will be deduped, or resolving the dedup key before enriching.
| errorCode:MTAdErrorCodeMissingAvailStart | ||
| message:@"avail missing startTimeInSeconds; inferred from first ad"]]; | ||
| MTAd *firstAd = avail.ads.firstObject; | ||
| if (firstAd && br.startTimeMs == 0.0) { |
There was a problem hiding this comment.
This treats a legitimate zero-position break the same as an "unset" one. br.startTimeMs is a primitive NSTimeInterval with no sentinel for "unset" — a preroll break parsed at position 0 (a completely normal, correct value) looks identical here to "never set." When the avail is missing startTimeInSeconds (the A8 case) and the break's own startTimeMs happens to be 0.0 because it's a real preroll, this overwrites the correct value with firstAd.startTimeMs, desyncing the break from its own pods (which still start at 0). The existing test testA8_missingAvailStartTime_logsAndQueuesError actually constructs this exact 0→42000ms overwrite but only asserts on the error code/count, not on the resulting startTimeMs, so it wouldn't catch a regression here. Consider gating this fallback on whether the break's start was itself inferred/unknown, rather than on the raw value being 0.0.
| if ([consumed containsObject:avail]) continue; | ||
| if (!avail.hasStartTime) continue; | ||
| NSTimeInterval delta = fabs(avail.startTimeMs - br.startTimeMs); | ||
| if (delta <= bestDelta) { |
There was a problem hiding this comment.
Non-strict <= means a tied delta lets a later avail silently replace an earlier match. bestDelta starts at the full tolerance and this loop updates best/bestDelta whenever delta <= bestDelta, so two avails equidistant from br.startTimeMs resolve to whichever is later in iteration order rather than the first-found. Contrast with the sibling helper elsewhere in this file that uses bestDelta = INFINITY with a strict <, keeping the first-found candidate on ties — this loop is inconsistent with that convention. avails isn't sorted or deduped before this pass, so ties are reachable whenever the tracking API returns two unconsumed avails at the same tolerance-boundary distance. Untested today (no test exercises two candidates at an equal delta). Suggest switching to strict < to match the sibling helper's tie-breaking behavior.
| + (id)createAdTracker { | ||
| // Dynamic class loading with graceful fallback | ||
| Class trackerClass = NSClassFromString(@"NRTrackerIMA"); | ||
| + (id)createAdTrackerForConfig:(NRAdConfig *)adConfig { |
There was a problem hiding this comment.
NRVAVideo.h still declares the old no-arg +(id)createAdTracker; selector, which no longer has an implementation after this rename to createAdTrackerForConfig:. (The header itself isn't touched by this PR's diff, so I can't anchor a comment directly on it — flagging here instead.) Any caller invoking [NRVAVideo createAdTracker] per the still-published public header would crash at runtime with "unrecognized selector sent to class." Nothing in this repo calls the bare selector today, so it's not an active crash, but the public API contract is broken silently. Worth removing (or updating) the stale declaration in NRVAVideo.h line 178 in the same PR.
|
|
||
| + (instancetype)fromJSONData:(NSData *)data error:(NSError **)error { | ||
| if (data.length == 0) { | ||
| if (error) *error = [NSError errorWithDomain:@"MTTrackingResponse" code:1 userInfo:@{NSLocalizedDescriptionKey: @"empty body"}]; |
There was a problem hiding this comment.
Ad-hoc error domain/codes instead of the module's own MTTrackingErrorDomain/MTTrackingErrorCode. fromJSONData: builds NSErrors with domain string @"MTTrackingResponse" and raw codes 1/2, rather than the shared error domain used everywhere else in this module (e.g. MTTrackingClient.m). Any caller that switches on MTTrackingErrorDomain/MTTrackingErrorCodeInvalidResponse or ParseFailed (as NRTrackerMediaTailor.m does for the client's errors) will never match an error from an empty-body or non-object JSON response here — it's silently misclassified into an unlisted domain. Suggest reusing MTTrackingErrorDomain with an appropriate MTTrackingErrorCode here for consistency.
| // (IMA drives itself via the IMA SDK, so it needs neither.) The host | ||
| // still calls -startTrackingWithSchedule: once it has the manifest + | ||
| // tracking JSON — MediaTailor is a passive schedule observer. | ||
| if (adTracker && config.adConfig.type == NRAdTrackerTypeMediaTailor) { |
There was a problem hiding this comment.
Shared infrastructure hardcodes a MediaTailor-specific branch rather than a generic per-tracker configuration contract. This if (adTracker && config.adConfig.type == NRAdTrackerTypeMediaTailor) block lives in core addPlayer: and pushes MediaTailor-specific fields (adSegmentPrefix, trackingUrl) via KVC. It also checks respondsToSelector:@selector(setAdSegmentPrefix:) and then calls setValue:forKey:@"adSegmentPrefix" — two different identifiers (a selector and a KVC key string) that aren't tied together, so a future rename of one and not the other would pass the guard and then crash with NSUnknownKeyException. Longer-term, a third tracker type will need another type-switch branch bolted onto this same shared method. A -configureWithAdConfig:player: method that each tracker subclass implements (called uniformly, without NRVAVideo.m knowing tracker-specific field names) would avoid both the KVC/selector drift risk and the growing type-switch.
skatti97
left a comment
There was a problem hiding this comment.
Follow-up pass focused purely on code quality (reuse + simplification), no correctness bugs in this batch. 6 findings below.
| + (instancetype)fromDictionary:(NSDictionary *)dict { | ||
| if (![dict isKindOfClass:[NSDictionary class]]) return nil; | ||
|
|
||
| NSString *availId = [dict[@"availId"] isKindOfClass:[NSString class]] ? dict[@"availId"] : nil; |
There was a problem hiding this comment.
Same isKindOfClass:[NSString class]] ? ... : nil guard hand-copied 9 times. This exact pattern — extract a dictionary value, verify it's actually a string, fall back to nil — appears here and at MTAvail.m:32, MTAd.m:44, MTNonLinearAvail.m:26, and MTTrackingEvent.m:28-30,45: nine call sites across four files, each re-deriving the identical check. A small shared helper (e.g. MTStringOrNil(dict, key)) would remove the duplication and give one place to harden the check later (trimming whitespace, tolerating NSNull, etc.) instead of nine.
| self.currentState = MTAdStateInPod; | ||
| } | ||
|
|
||
| - (void)checkQuartilesForPod:(MTAdPod *)pod atPositionMs:(NSTimeInterval)positionMs { |
There was a problem hiding this comment.
Three near-identical blocks differing only by a threshold and a flag name. checkQuartilesForPod: repeats the same if (progress >= X && !pod.hasFiredQN) { pod.hasFiredQN = YES; ... } shape three times for 0.25/Q1, 0.50/Q2, 0.75/Q3 — nothing differs structurally between them. Iterating a small static array of (threshold, quartileNumber) pairs in one loop would remove the copy-paste and the risk of a future edit mismatching a threshold with the wrong flag (or a 4th checkpoint being added as a 4th near-identical block).
| // Firing flags | ||
| @property (nonatomic, assign) BOOL hasFiredStart; | ||
| @property (nonatomic, assign) BOOL hasFiredEnd; | ||
| @property (nonatomic, assign) BOOL hasFiredAdStart; |
There was a problem hiding this comment.
hasFiredAdStart/hasFiredQ1/hasFiredQ2/hasFiredQ3 (lines 47-50) are declared but never read or written anywhere in the module. Quartile/ad-start firing is actually tracked one level down on MTAdPod, which has its own working copies of these same flags that MTPlayheadStateMachine actually uses. The only place these four properties are touched at all is a default-value assertion in MTAdDecodeTests.m (always-passing, since nothing ever sets them). Beyond being dead weight, they're actively misleading — a future contributor skimming this header would reasonably assume break-level quartile tracking happens here, and either build on top of these stale fields or duplicate the (already-correct) pod-level tracking a second time. Suggest deleting all four properties and their corresponding test assertions.
| #pragma mark - Player attachment / KVO | ||
|
|
||
| - (void)setPlayer:(id)player { | ||
| if (self.isDisposed) { return; } |
There was a problem hiding this comment.
if (self.isDisposed) { return; } is copy-pasted into 11 methods (this line plus 192, 214, 234, 248, 256, 264, 275, 283, 290, 297), including every MTPlayheadStateMachineDelegate callback. It works today, but it's structurally optional — a new delegate/public method added later that forgets to paste this guard would silently act on a torn-down tracker. Since dispose already tears down self.stateMachine and detaches the player, setting self.stateMachine.delegate = nil before doing so would make the six delegate callbacks structurally impossible to invoke post-dispose, removing the need for the guard on those six specifically (the public entry points like setPlayer:/startTrackingWithSchedule: would still need it, since those can be called directly regardless of the delegate link).
|
|
||
| @property (nonatomic, strong, readonly) MergedSchedule *schedule; | ||
| @property (nonatomic, assign, readwrite) NSTimeInterval playheadPollInterval; | ||
| @property (nonatomic, assign, readwrite) MTAdState currentState; |
There was a problem hiding this comment.
currentState is a manually-synced cache instead of a computed value. It's independently assigned at (at least) 4 separate spots in this file — line 117 (entering a break), 147 (exiting), 158 (entering a break, no-fill branch), 167 (entering a pod) — rather than being derived on read from currentAdBreak/currentAdPod/isNoFill, which the object already tracks. A future transition path that forgets to update currentState would leave it stale while the rest of the object has already moved on, with nothing to catch the mismatch. Computing it on read instead (if (!self.currentAdBreak) return MTAdStateContent; if (self.currentAdBreak.isNoFill) return MTAdStateNoFill; return self.currentAdPod ? MTAdStateInPod : MTAdStateInBreak;) removes this whole category of "forgot to update it" bug.
| NSMutableString *_locationText; | ||
|
|
||
| // <BaseURL> text accumulators (depth-aware). | ||
| BOOL _capturingPeriodBaseURL; |
There was a problem hiding this comment.
Four parallel (BOOL capturing_, NSMutableString *_Text) pairs (lines 69-74, plus the top-level pair above) track what's structurally a single piece of state: "which BaseURL level am I currently inside, if any." The same 4-way if/else-if chain over these pairs is repeated in didStartElement (~173-192), foundCharacters (~280-296), and didEndElement (~314-344) — their mutual exclusivity is only an implicit convention enforced by re-deriving the same chain three times. A single _baseURLCaptureTarget enum (None/TopLevel/Period/Adaptation/Representation) plus one shared text accumulator would collapse all three call sites to a single switch/if, and make "at most one level capturing at a time" a structural invariant instead of a convention three separate methods have to independently maintain.
Summary
Adds
NRMediaTailorTracker— a new iOS / tvOS SDK module that emits New Relic ad telemetry for AWS MediaTailor server-side-stitched ads playing throughAVPlayer. Mirrors the role and event surface ofNRTrackerIMA: attach to the player, hear about ads, emit events. Passive observer — no UI, no ad decisioning, no beacon firing.Before: customers playing MediaTailor streams got
CONTENT_*events fromNRAVPlayerTrackerand zero ad-side telemetry. Ads played; New Relic was blind to them.After: customers get the full ad event vocabulary (
AD_BREAK_START,AD_REQUEST,AD_START,AD_QUARTILE×3,AD_END,AD_BREAK_END,AD_ERROR, plusAD_PAUSE/AD_RESUME/AD_SKIP) with full metadata:availId,creativeId,adSystem,vastAdId,adTitle,creativeSequence,skipOffset,adProgramDateTime,availProgramDateTime,noFill,podCountMismatch,isBumper.What's in the module
The module owns six subsystems and 17 source files, all behind a single public class (
NRTrackerMediaTailor) re-exported through the umbrella header.MTDetector)MTManifestParserprotocol +MTHlsParser+MTDashParser)MTTrackingClient)GET /v1/tracking/<sessionId>with properNextTokenround-trip pagination, 5 s timeout, one retry on transient network errors, cancellableMTAdScheduleMerger)MTPlayheadStateMachine)The customer app's integration is four method calls: parse manifest → fetch tracking JSON → merge → install schedule. Everything else (HTTP lifecycle, pagination, retry, dedup, state transitions, attribute enrichment, KVO pause/resume) is module-internal.
Format support
MTHlsParser— default forAVPlayerconsumersMTDashParser— for customers using third-party players (THEOplayer, Bitmovin, Shaka) with their own DASH playbackMTManifestParserprotocol for non-standard CDN layoutsParity with the Android reference + 15 documented fixes
Module is in parity with
video-agent-android/NRMediaTailorTracker, with all 15 bugs fromNRMediaTailorTracker_BUGS_TO_FIX.mdfixed:Group A (atomic-fact violations in Android, fixed in iOS):
?t=<wallclock>cache-buster — replaced with properNextTokenpaginationAD_BREAK_START → AD_ERROR(NO_FILL) → AD_BREAK_END, noAD_START)(availId, availProgramDateTime ?? startTimeMs)for live HLS sliding-window stabilitystartTimeInSecondslogs warning + queuesMISSING_AVAIL_START, then falls backGroup B (additional bugs surfaced by cross-referencing AWS atomic facts):
NextTokenround-tripped (entire pagination contract Android ignored)creativeIdis primary identity;(availId, adId)composite fallbackMTTrackingClient.h)trackingEvents[*].startTimeInSecondsdocumented asrelativeToAdStartMsto prevent semantic confusionAD_ERRORevent vocabulary with 7-valueMTAdErrorCodeavailProgramDateTime/adProgramDateTimealways emitted (empty when nil) for reliable live correlationAnti-pattern guardrails
10 explicit non-goals documented in
CONTRIBUTING.mdand at the top ofNRTrackerMediaTailor.m:BEHIND_LIVE_EDGEetc.)Platform support
Verification
NRMediaTailorTracker.frameworkExamples/iOS/SimplePlayerWithAds(the ad-tracking example), alongside the existing IMA pathRelease-pipeline integration
The new module is wired into the same release machinery as
NRAVPlayerTrackerandNRIMATracker:build-xcframeworks.shnow producesNRMediaTailorTracker.xcframeworkalongside the existing three modules' xcframeworks for inclusion in the GitHub Releasexcframeworks.zip.github/workflows/ios-publish.ymlvalidates and publishesNRMediaTailorTracker.podspecto CocoaPods trunk.github/workflows/ios-release.ymlversion-bumpsNRMediaTailorTracker.podspecand the hardcoded@"<version>"in-[NRTrackerMediaTailor getTrackerVersion]A pre-existing telemetry bug was surfaced and fixed while wiring
ios-release.yml: the baseNRVideoTracker.getTrackerVersionreturnsNSNull, so without a per-module override the new module's events would reporttrackerVersion = NSNullin NRDB. Added the override in commit1909b13.Test plan
xcodebuild testagainstNRMediaTailorTrackerTestson iPhone 16 simulator (expect 132/132 passing)-enableCodeCoverage YESshows ≥70% line coverage on the framework target (latest local: 89.92%)NRMediaTailorTracker-iOSscheme againstiphonesimulatorSDKNRMediaTailorTracker-tvOSscheme againstappletvsimulatorSDKcd Examples/iOS/SimplePlayerWithAds && pod install, open the workspace, set the MediaTailor session URL viadefaults write com.newrelic.SimplePlayerWithAds MediaTailorSampleURL "<your-url>"(or theMT_SAMPLE_URLenv var), run the app, tap MediaTailor Sample, confirm in a proxy capture that/v1/tracking/<sessionId>requests round-tripnextTokenbetween consecutive callsAD_BREAK_START → AD_START → 3× AD_QUARTILE → AD_END → AD_BREAK_ENDfires for at least one ad break on the integration stream, and thattrackerVersion = "4.2.0"(notNSNull) on every eventNotes for reviewers
EXT-X-TARGETDURATION.NRMediaTailorTracker_*.mdplanning / reference docs at the repo root are intentionally untracked per project convention and are not part of this PR.