AVPlayer Quality Test¶
The AVPlayerQualityTest plays a video stream with AVFoundation's AVPlayer and scores the playback with the ITU-T Rec. P.1203 model. It is the iOS counterpart of the Android ExoPlayer Quality Test, and reports its results under the subject avplayer.
The test plays for a configurable duration, then stops playback, builds a P.1203 input report from what the player did, and computes the quality scores on the device. Have a look at AVPlayerTestView.swift in the demo app for a complete example.
Starting the Test¶
In the example below the test plays an HLS stream for 30 seconds. The maximum test duration is a timeout for the whole test in case of network problems, and the analysis duration is a timeout for computing the statistics once playback is over.
import SurfmeterQualitySDK
let test = try AVPlayerQualityTestBuilder(manifest: URL(string: "https://example.com/stream.m3u8")!)
.setMaxPlaybackDuration(durationMs: 30_000)
.setMaxTestDuration(durationMs: 45_000)
.setMaxAnalysisDuration(durationMs: 10_000)
.setProgressInterval(intervalMs: 1000)
.setResultListener(listener: self)
.build()
try test.start()
Both build() and start() throw, so wrap them in a do/catch and handle the error. build() rejects invalid combinations, for example a maximum test duration shorter than the maximum playback duration. start() throws when the test has already been started.
To stop a running test early, call stop(). If playback was already running, the test finishes normally and you get a result for what was played so far. If it had not started yet, you get an error instead.
Builder Options¶
AVPlayerQualityTestBuilder is created with the manifest URL and supports the following methods, all of which return the builder so they can be chained:
setMaxPlaybackDuration(durationMs:)– maximum playback duration in milliseconds, 30000 by defaultsetMaxTestDuration(durationMs:)– maximum test duration in milliseconds, 45000 by default. Must be at least the playback durationsetMaxAnalysisDuration(durationMs:)– timeout for computing the statistics, in milliseconds, 10000 by defaultsetProgressInterval(intervalMs:)– how oftenonPlaybackProgressis called, in milliseconds, 1000 by defaultsetResultListener(listener:)– theQualityTestResultListenerthat receives results, errors and state changessetCalculationSettings(calculationSettings:)– P.1203 calculation settings, see belowattachToView(view:)– attach the player to aUIViewso the video is visible during the testsetAllowsBackgroundPlayback(_:)– allow playback and measurement to continue after the host app enters the background,falseby defaultsetReportedDisplaySize(width:height:)– the display size reported to P.1203, in device pixelssetMetadata(_:)– arbitrary key-value pairs that are stored alongside the measurement
There is also a setExternalPlayer(player:) method for handing the test an AVPlayer you created yourself. See Using an External Player for its lifecycle rules. It cannot be combined with attachToView; build() throws if both are set.
Tip
You can get test URLs from the hls.js demo. Note that AVPlayer supports HLS and progressive MP4, but not DASH, so the DASH streams that work with the Android SDK will not play here.
Reported Display Size¶
P.1203 scores partly on how far the video has to be scaled to fit the display, so the model needs to know which display the measurement stands for.
This is deliberately independent of attachToView. A test running in a small preview, or with no view at all, is usually meant to represent what a full-screen viewing would look like, so the SDK does not measure the player's actual size. Left unset, the report assumes a 1920x1080 display.
Calculation Settings¶
Calculation settings select modules and adjustments within the combined video QoE calculation. In the Video QoE model, pv is the video-quality module. It produces a video MOS value for each second (O.22). The pq module then combines those video values with the audio quality and stalling events to produce the final session MOS (O.46). See the P.1203 module diagram for this data flow.
You normally do not need to call setCalculationSettings. The iOS SDK uses these defaults:
-
pv:P12043BitstreamMode0. Despite this legacy internal name, it selects the ITU-T P.1204.1 Mode 0 video-quality model. This metadata-based model uses the codec, bitrate, resolution, frame rate, and reported display size; it does not inspect the encoded bitstream. -
pq:P1203PqExtended. This is the P.1203.3 integration model with additional diagnostic outputs. -
amendment1Audiovisual:true. This increases the effect of very low audiovisual quality on the final MOS. -
amendment1Stalling:true. This increases the effect of stalling on the final MOS.
You can override individual defaults as follows:
try builder.setCalculationSettings(calculationSettings: [
"pv": "P1203PvRetrained",
"pq": "P1203PqExtended",
"amendment1Audiovisual": true,
"amendment1Stalling": true,
"amendment1App2": false
])
The current on-device calculator supports P12043BitstreamMode0 and P1203PvRetrained for pv. P1203PvRetrained uses P.1203.1 Mode 0 with retrained coefficients for additional codecs. The P.1203 extensions and variants page compares both models and their supported inputs.
P1203PqExtended is the supported pq value. amendment1App2 applies an Appendix 2 adjustment when externally calculated bitstream- or pixel-based video scores are used; leave it false for the metadata-based models above.
The builder's validation also accepts the server-side model identifiers P1203PvExtended, P1203PvHveiExtended, and P1203PqM, as well as the longMode key. The current on-device statistics calculator does not implement these model choices, and longMode does not change its calculation. Do not use them for an AVPlayerQualityTest.
Codec and Bitrate Fallbacks¶
AVFoundation can expose a stream codec using a FourCC value that the quality model does not recognize. In that case, the SDK logs a warning and uses H.264 for an unknown video codec or AAC for an unknown audio codec. If AVFoundation reports no video bitrate, the SDK uses the same resolution-based nominal bitrate estimate as the Android SDK and logs another warning.
These values keep the quality calculation from returning an empty result, but they are estimates. Filter the app's Console logs on the AVPlayerObserver category when you need to check whether a result used a codec or bitrate fallback.
Using an External Player¶
Use setExternalPlayer(player:) when your app already owns and presents the player. Load the stream passed to the builder into that player, then start the test before starting playback so the complete interval is measured:
import AVFoundation
import SurfmeterQualitySDK
let manifestURL = URL(string: "https://example.com/stream.m3u8")!
let player = AVPlayer(url: manifestURL)
let test = try AVPlayerQualityTestBuilder(manifest: manifestURL)
.setExternalPlayer(player: player)
.setResultListener(listener: self)
.build()
try test.start()
player.play()
The test observes the external player but does not start it, pause it, replace its current item, or release it. Your app remains responsible for the player throughout the test. For background playback, follow the additional external-player setup below.
Receiving Test Results¶
To receive the outcomes of the test, your class must conform to the QualityTestResultListener protocol. This protocol includes methods for handling successful results, errors, state changes, and playback progress.
import SurfmeterQualitySDK
class TestRunner: QualityTestResultListener {
func onTestResult(_ result: [String: AnyHashable]) {
// the finished measurement report
}
func onTestError(_ error: String) {
// the test could not be completed
}
func onTestStateChanged(_ state: QualityTestState) {
// the test moved to a new state
}
func onPlaybackProgress(_ progress: Int) {
// playback progress in percent
}
}
Exactly one of onTestResult or onTestError is called per test run. The SDK holds a strong reference to the listener until that final callback has been delivered, then releases it, so a listener that also owns the test does not leak.
Test Success¶
When the test completes, onTestResult is called with the full measurement report:
func onTestResult(_ result: [String: AnyHashable]) {
guard let stats = result["statistic_values"] as? [String: AnyHashable] else { return }
print("Overall MOS: \(stats["p1203OverallMos"] ?? "n/a")")
}
The report has the same shape as the one the Android SDK produces, so both platforms can be read the same way. The quality scores are under statistic_values, and include p1203OverallMos, p1203AverageVideoQuality, p1203AverageAudioQuality and p1203StallingQuality, alongside playback statistics such as initialLoadingDelay, averageVideoBitrate and averageStallingTime. The full P.1203 computation that produced them is under statistics_input, and the raw input report and per-segment performance data are under client_reports.
The values correspond to the measurement data, so please refer to that page for a description of each one.
To serialize the report, for example to store it or to display it:
if let data = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted),
let json = String(data: data, encoding: .utf8) {
print(json)
}
Test Errors¶
If the test cannot be completed, onTestError is called with a message instead:
This happens when the test is stopped before playback started, or when computing the statistics times out.
Playback Progress¶
While the video is playing, onPlaybackProgress reports the position as a percentage of the expected playback duration, at the interval you set through setProgressInterval. It is called once more with 100 when playback finishes.
Test State Changes¶
You can follow the lifecycle of the test through onTestStateChanged:
The video test passes through the following states, in this order:
STATE_READY— the test has been built but not startedSTATE_STARTING—start()was called, setup is in progressSTATE_PLAYING— playback is runningSTATE_ANALYZING— playback is over, statistics are being computedSTATE_END— the test is finished, the result callback follows
If the test is aborted, it moves to STATE_ABORTED instead.
Showing the Video¶
By default the test plays without any visible output, which is enough for a measurement. To show the video, hand the test a view to attach the player layer to:
The player layer is sized to the view's bounds when the test is built, so make sure the view has its final size by then.
Background Execution¶
AVPlayerQualityTest can continue when the user leaves the app after a test has started. This is the iOS counterpart to Android background measurements, but iOS does not provide a service or wake-lock equivalent. The host app must qualify for background media playback and keep a real media playback session active.
Background playback is off by default. Enable it only for an app whose purpose includes media playback. It does not add periodic scheduling, launch a test while the app is suspended, or enable background execution for VideoQualityTest and WebQualityTest.
Host App Setup¶
In Xcode, open the app target's Signing & Capabilities settings, add the Background Modes capability, and select Audio, AirPlay, and Picture in Picture. This adds the following entry to the app's Info.plist:
Before starting the test, configure and activate the app's shared audio session for playback:
import AVFoundation
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback)
try audioSession.setActive(true)
The SDK does not change the shared AVAudioSession, because doing so could conflict with the host app's audio and interruption policy. The app is also responsible for handling interruptions, route changes, and deactivating the session after playback when appropriate.
Do not use silent audio solely to keep the app running. Background modes must match the app's real function and the playback presented to the user.
SDK-Owned Player¶
For the player created by the SDK, enable background playback on the builder:
let test = try AVPlayerQualityTestBuilder(manifest: manifestURL)
.setAllowsBackgroundPlayback(true)
.setMaxPlaybackDuration(durationMs: 60_000)
.setMaxTestDuration(durationMs: 75_000)
.setResultListener(listener: self)
.build()
try test.start()
The SDK sets the player's audiovisualBackgroundPlaybackPolicy to .continuesIfPossible. This works with headless playback and with a player attached through attachToView(view:).
External Player Background Playback¶
The SDK does not change a caller-owned player. Set its background policy before building the test:
let player = AVPlayer(url: manifestURL)
player.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
let test = try AVPlayerQualityTestBuilder(manifest: manifestURL)
.setExternalPlayer(player: player)
.setAllowsBackgroundPlayback(true)
.setResultListener(listener: self)
.build()
try test.start()
player.play()
Validation and Completion¶
When background playback is enabled, build() checks that the host app declares the audio background mode. For an external player it also checks that audiovisualBackgroundPlaybackPolicy is .continuesIfPossible. Either missing requirement throws QualityTestError.backgroundPlaybackUnavailable, with error code 1015.
The SDK cannot verify whether the process-wide audio session is active and correctly configured. If it is not, iOS can suspend playback after the app enters the background.
When playback finishes, the SDK requests a short UIKit background task while it computes the statistics, stores the report in the measurement queue, and delivers the final callback. If iOS does not grant that time, or the background task expires, the test reports an error and records the run as aborted instead of returning incomplete or empty statistics.