After enabling the developer mode in Oqee in my previous post, the next logical step was obvious: removing the ads that play at the beginning and middle of a show.
On iOS, it’s trivial to use something like NextDNS to block ad servers at the network level. But I wanted a permanent, native solution that would seamlessly work on the tvOS app on my Apple TV, where DNS tweaking is a bit more annoying to manage..
I dumped the app again and started looking for ad-related strings in IDA. The app is massive, and there were thousands of ad-related cross-references. To speed things up, I hooked up ida-pro-mcp with a local Qwen 3.5 instance running in LM Studio. It worked surprisingly well for navigating the noise, and I mapped out the ad architecture pretty quickly.
Here is a simplified diagram of how ads work in the Oqee app:
flowchart LR
subgraph iOS["iOS Device · Oqee"]
direction TB
presenter["<b>OQPlayerAdsPresenterIMA</b><br/>ads coordinator"]
imaSDK["<b>OQImaManager</b><br/>Google IMA SDK"]
antiSkip["<b>OQAntiAdSkippingManager</b><br/>seek lock enforcer"]
avPlayer["<b>AVPlayer</b><br/>ad + content rendering"]
presenter --> imaSDK --> avPlayer
end
subgraph backend["OQEE Backend"]
direction TB
api["<b>api.oqee.net</b><br/>/api/v1/replay/{id}/playback_infos"]
antiApi["<b>api.oqee.net</b><br/>/api/v1/live/anti_adskipping/"]
vizChoice(["<b>VizChoice</b><br/>VAST/VMAP builder"])
vizChoice -."196 KB VMAP XML<br/>inline in JSON response".-> api
end
subgraph cdn["OQEE Media CDN"]
direction TB
cdnHost["<b>replay-01.bzn.oqee.net</b><br/>oqee-static/barkers"]
mp4["MP4 progressive<br/>free_cine_hd.mp4"]
dash["DASH adaptive<br/>playlist_*.mpd"]
cdnHost --> mp4 & dash
end
presenter --"POST playback_infos"--> api
api --"JSON + VMAP XML"--> imaSDK
imaSDK --"GET ad media"--> cdnHost
mp4 & dash --"ad stream"--> avPlayer
antiSkip -."GET anti_adskipping<br/>periods".-> antiApi
Network reconnaissance
I fired up a MITM proxy to see what the app was requesting when launching a stream. Immediately, I caught a request to https://api.oqee.net/api/v1/svod/offers/906907/playback_infos returning this JSON payload:
{
"success": true,
"result": {
"type": "hybrid",
"drm": "fairplay",
"stream_type": "hls",
"cipher": "cbcs",
"media_url": "https://replay-02.oqee.net/pxd2/oqee-vod-5003/2476563/hls_master_945f97d2.m3u8",
"license_server": "https://api.oqee.net/api/v1/avod/license/vygj8DaRsaMokMJn/fairplay",
"fairplay_certificate": "https://api.oqee.net/staticfiles/fairplay/free.cer",
"subtitles": {},
"thumbnails": {
"width": 426,
"height": 240,
"nb_rows": 5,
"pattern": "https://replay-02.oqee.net/pxd2/oqee-vod-5003/2476563/tile_426x240_945f97d2_%06d.jpg",
"interval": 10,
"nb_columns": 4
},
"ads_info": {
"ads_format": "vmap",
"payload": "<?xml version='1.0' encoding='utf-8'?>\n<vmap:VMAP xmlns:vmap=\"http://www.iab.net/vmap-1.0\" version=\"1.0\">.....</vmap:VMAP>"
},
"position": 6106,
"pre_medias": [
{
"type": "image",
"url": "https://replay-03.oqee.net/oqee-static/arcom/rating_12.png",
"image": {
"display_duration_s": 4
}
}
],
"program_id": 137184,
"media_duration": 6829
}
}
The interesting part is the ads_info.payload. It contains a massive VMAP (Video Multiple Ad Playlist) XML document:Click to expand the raw
VMAP XML<?xml version='1.0' encoding='utf-8'?>
<vmap:VMAP
xmlns:vmap="http://www.iab.net/vmap-1.0" version="1.0">
<vmap:AdBreak breakId="cue_point--789000348" breakType="linear" timeOffset="00:29:28">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="1">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd">
<Ad id="breakIntro" sequence="1">
<InLine>
<AdSystem>VizChoice</AdSystem>
<AdTitle>Oqee Cine Break Intro</AdTitle>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:03</Duration>
<MediaFiles>
<MediaFile delivery="progressive" width="1920" height="1080" type="video/mp4">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/free_cine_hd.mp4</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/dash+xml">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.mpd</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/x-mpegURL">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.m3u8</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-breakIntro">https://vizchoice.viznet.tv/ads/t?...</Impression>
</InLine>
</Ad>
<Ad id="638286129" sequence="2">
<InLine>
<AdSystem>DCM</AdSystem>
<AdTitle>In-Stream Video</AdTitle>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-638286129">https://vizchoice.viznet.tv/ads/t?...</Impression>
<Creatives>
<Creative id="253615465" AdID="253615465-1" sequence="1">
<Linear>
<Duration>00:00:15</Duration>
<TrackingEvents>
<Tracking event="start">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="firstQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="midpoint">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="thirdQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="complete">https://vizchoice.viznet.tv/ads/t?...</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickTracking>https://vizchoice.viznet.tv/ads/t?...</ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile id="1048790128-22" delivery="progressive" width="1280" height="720" type="video/mp4" bitrate="2012" scalable="true" maintainAspectRatio="true">https://gcdn.2mdn.net/videoplayback/id/8cf51eecac5940c7/itag/22/source/web_video_ads/xpc/EgVovf3BOg%3D%3D/ctier/L/acao/yes/ip/0.0.0.0/ipbits/0/expire/3920728750/sparams/id,itag,source,xpc,ctier,acao,ip,ipbits,expire/signature/7277192E920D946AC09E72B44142450DABC7AB3A.72DF870F0094B6468C95011B129607AFA89A9AD1/key/ck2/file/file.mp4</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="cue_point--789000350" breakType="linear" timeOffset="01:16:34">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="2">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd">
<Ad id="breakIntro" sequence="1">
<InLine>
<AdSystem>VizChoice</AdSystem>
<AdTitle>Oqee Cine Break Intro</AdTitle>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:03</Duration>
<MediaFiles>
<MediaFile delivery="progressive" width="1920" height="1080" type="video/mp4">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/free_cine_hd.mp4</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/dash+xml">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.mpd</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/x-mpegURL">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.m3u8</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-breakIntro">https://vizchoice.viznet.tv/ads/t?...</Impression>
</InLine>
</Ad>
<Ad id="638482837" sequence="2">
<InLine>
<AdSystem>DCM</AdSystem>
<AdTitle>In-Stream Video</AdTitle>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-638482837">https://vizchoice.viznet.tv/ads/t?...</Impression>
<Creatives>
<Creative id="253627543" AdID="253627543-1" sequence="1">
<Linear>
<Duration>00:00:15</Duration>
<TrackingEvents>
<Tracking event="start">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="firstQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="midpoint">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="thirdQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="complete">https://vizchoice.viznet.tv/ads/t?...</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickTracking>https://vizchoice.viznet.tv/ads/t?...</ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile id="1049541392-22" delivery="progressive" width="1280" height="720" type="video/mp4" bitrate="1990" scalable="true" maintainAspectRatio="true">https://gcdn.2mdn.net/videoplayback/id/3b72ebdd33fea586/itag/22/source/web_video_ads/xpc/EgVovf3BOg%3D%3D/ctier/L/acao/yes/ip/0.0.0.0/ipbits/0/expire/3920728797/sparams/id,itag,source,xpc,ctier,acao,ip,ipbits,expire/signature/6CFA9E80F63EE63D7AC23ACE56590D5FC93AE927.A4235CB1146043D10E7493AAE116C687B62F34C3/key/ck2/file/file.mp4</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="0.0.0.1338822117" breakType="linear" timeOffset="start">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="3">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd">
<Ad id="breakIntro" sequence="1">
<InLine>
<AdSystem>VizChoice</AdSystem>
<AdTitle>Oqee Cine Break Intro</AdTitle>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:03</Duration>
<MediaFiles>
<MediaFile delivery="progressive" width="1920" height="1080" type="video/mp4">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/free_cine_hd.mp4</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/dash+xml">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.mpd</MediaFile>
<MediaFile delivery="streaming" width="1920" height="1080" type="application/x-mpegURL">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.m3u8</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-breakIntro">https://vizchoice.viznet.tv/ads/t?...</Impression>
</InLine>
</Ad>
<Ad id="638482837" sequence="2">
<InLine>
<AdSystem>DCM</AdSystem>
<AdTitle>In-Stream Video</AdTitle>
<Error>https://vizchoice.viznet.tv/ads/t?...</Error>
<Impression id="vizchoice-638482837">https://vizchoice.viznet.tv/ads/t?...</Impression>
<Creatives>
<Creative id="253627543" AdID="253627543-1" sequence="1">
<Linear>
<Duration>00:00:15</Duration>
<TrackingEvents>
<Tracking event="start">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="firstQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="midpoint">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="thirdQuartile">https://vizchoice.viznet.tv/ads/t?...</Tracking>
<Tracking event="complete">https://vizchoice.viznet.tv/ads/t?...</Tracking>
</TrackingEvents>
<VideoClicks>
<ClickTracking>https://vizchoice.viznet.tv/ads/t?...</ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile id="1049541392-22" delivery="progressive" width="1280" height="720" type="video/mp4" bitrate="1990" scalable="true" maintainAspectRatio="true">https://gcdn.2mdn.net/videoplayback/id/3b72ebdd33fea586/itag/22/source/web_video_ads/xpc/EgVovf3BOg%3D%3D/ctier/L/acao/yes/ip/0.0.0.0/ipbits/0/expire/3920728797/sparams/id,itag,source,xpc,ctier,acao,ip,ipbits,expire/signature/6CFA9E80F63EE63D7AC23ACE56590D5FC93AE927.A4235CB1146043D10E7493AAE116C687B62F34C3/key/ck2/file/file.mp4</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="pause.0.393383558" breakType="linear" timeOffset="00:00:00">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="4">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"/>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="pause.1.1224855718" breakType="linear" timeOffset="00:00:00">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="5">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"/>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="pause.2.1614836030" breakType="linear" timeOffset="00:00:00">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="6">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"/>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="pause.3.511341525" breakType="linear" timeOffset="00:00:00">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="7">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"/>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
<vmap:AdBreak breakId="pause.4.218063441" breakType="linear" timeOffset="00:00:00">
<vmap:AdSource allowMultipleAds="true" followRedirects="true" id="8">
<vmap:VASTAdData>
<VAST
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"/>
</vmap:VASTAdData>
</vmap:AdSource>
<vmap:TrackingEvents>
<vmap:Tracking event="breakStart">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
<vmap:Tracking event="breakEnd">https://vizchoice.viznet.tv/ads/t?...</vmap:Tracking>
</vmap:TrackingEvents>
</vmap:AdBreak>
</vmap:VMAP>
If we look closely at the document, we can see several ad breaks defined, each with breakType="linear". This tells the player to physically pause the content while the ad is rolling. The timeOffset determines exactly when this happens:
- Pre-roll (
timeOffset="start"): Plays a 3s Intro + a 15s DCM Ad - Mid-roll 1 (
timeOffset="00:29:28"): Plays a 3s Intro + a 15s DCM Ad - Mid-roll 2 (
timeOffset="01:16:34"): Plays a 3s Intro + a 15s DCM Ad

The app utilizes the Google IMA SDK (GoogleInteractiveMediaAds.framework) to parse this VMAP and handle the media fetching from viznet.tv, a common ad provider for streaming platforms in France.
When a user starts a show, the backend sends the playback info containing the VMAP XML. The IMA SDK parses this via OQPlayerAdsPresenterIMA.loadAdsContent and schedules the breaks (OQImaManager.setupVASTAndRequestAds()). When the timeline hits a cue point, the IMA SDK fetches the ad media and plays it in the AVPlayer.
Hooking the IMA SDK
Checking IDA, I found the exact method responsible for initializing the ad request inside the IMA framework: -[IMAAdsRequest initWithAdsResponse:adDisplayContainer:contentPlayhead:userContext:].
IMAAdsRequest *__cdecl -[IMAAdsRequest initWithAdsResponse:adDisplayContainer:contentPlayhead:userContext:](
IMAAdsRequest *self,
SEL a2,
id a3,
id a4,
id a5,
id a6)
{
id v10; x19
id v11; x20
id v12; x21
id v13; x22
IMAAdsRequest *v14; x24
NSString *v15; x0
NSString *adsResponse; x8
IMAAdsRequest *v17; x23
objc_super v19; [xsp+0h] [xbp-50h] BYREF
v10 = objc_retain(a3);
v11 = objc_retain(a4);
v12 = objc_retain(a5);
v13 = objc_retain(a6);
if ( !v10 )
{
NSLog(&cfstr_InvalidParamet_3.isa);
LABEL_8:
v17 = 0;
goto LABEL_9;
}
if ( !v11 )
{
NSLog(&cfstr_InvalidParamet_12.isa);
goto LABEL_8;
}
v19.receiver = self;
v19.super_class = (Class)&OBJC_CLASS___IMAAdsRequest;
v14 = -[IMAAdsRequest init](&v19, "init");
if ( v14 )
{
v15 = (NSString *)objc_msgSend(v10, "copy");
adsResponse = v14->_adsResponse;
v14->_adsResponse = v15;
objc_release(adsResponse);
objc_storeStrong((id *)&v14->_adDisplayContainer, a4);
objc_storeWeak((id *)&v14->_contentPlayhead, v12);
objc_storeStrong(&v14->_userContext, a6);
v14->_contentDuration = -1.0;
}
self = objc_retain(v14);
v17 = self;
LABEL_9:
objc_release(v13);
objc_release(v12);
objc_release(v11);
objc_release(v10);
objc_release(self);
return v17;
}
Since the full pipeline involves IMAAdsLoader and IMAAdsManager, I wrote a quick Theos tweak to log the initialization sequence and confirm that the VMAP payload we saw in MITM is indeed what’s being passed directly into IMAAdsRequest.
(Note: Hooking VSSubscriptionRegistrationCenter is required when running the app sandboxed, which I explained in my TF1 related post).
#import <substrate.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#include <mach-o/dyld.h>
#include <string.h>
#include <stdint.h>
#define TAG @"[OQAdsLogger]"
%hook IMAAdsRequest
- (instancetype)initWithAdsResponse:(NSString *)adsResponse
adDisplayContainer:(id)adDisplayContainer
avPlayerVideoDisplay:(id)avPlayerVideoDisplay
pictureInPictureProxy:(id)pipProxy
userContext:(id)userContext {
NSLog(@"%@-[IMAAdsRequest init] [PiP path]", TAG);
NSLog(@"%@ adDisplayContainer = %@", TAG, adDisplayContainer);
NSLog(@"%@ avPlayerVideoDisplay = %@", TAG, avPlayerVideoDisplay);
NSLog(@"%@ pictureInPictureProxy = %@", TAG, pipProxy);
NSLog(@"%@ VMAP payload (%lu bytes):\n%@",
TAG, (unsigned long)adsResponse.length, adsResponse);
id result = %orig;
NSLog(@"%@ -> IMAAdsRequest = %p", TAG, result);
return result;
}
- (instancetype)initWithAdsResponse:(NSString *)adsResponse
adDisplayContainer:(id)adDisplayContainer
contentPlayhead:(id)contentPlayhead
userContext:(id)userContext {
NSLog(@"%@-[IMAAdsRequest init] [non-PiP path]", TAG);
NSLog(@"%@ adDisplayContainer = %@", TAG, adDisplayContainer);
NSLog(@"%@ contentPlayhead = %@", TAG, contentPlayhead);
NSLog(@"%@ VMAP payload (%lu bytes):\n%@",
TAG, (unsigned long)adsResponse.length, adsResponse);
id result = %orig;
NSLog(@"%@ -> IMAAdsRequest = %p", TAG, result);
return result;
}
%end
%hook IMAAdsLoader
- (void)requestAdsWithRequest:(id)request {
NSLog(@"%@ -[IMAAdsLoader requestAdsWithRequest:]", TAG);
NSLog(@"%@ loader = %@", TAG, self);
NSLog(@"%@ request = %@", TAG, [request debugDescription]);
%orig;
NSLog(@"%@ requestAdsWithRequest dispatched", TAG);
}
%end
%hook IMAAdsManager
- (void)initializeWithAdsRenderingSettings:(id)renderingSettings {
NSLog(@"%@ -[IMAAdsManager initializeWithAdsRenderingSettings:]", TAG);
NSLog(@"%@ adsManager = %@", TAG, self);
NSLog(@"%@ renderingSettings = %@", TAG, renderingSettings ?: @"(nil — default)");
%orig;
NSLog(@"%@ VMAP parsed, ad breaks scheduled", TAG);
}
%end
%hook VSSubscriptionRegistrationCenter
- (void)setCurrentSubscription:(id)subscription
{
NSLog(@"Blocked VSSubscriptionRegistrationCenter");
NSLog(@"Subscription: %@", subscription);
return;
}
%end
Deploying this to the device and keeping an eye on the console gave me exactly what I wanted:
[OQAdsLogger]-[IMAAdsRequest init] [PiP path]
[OQAdsLogger] adDisplayContainer = <IMAAdDisplayContainer: 0x113c9bcc0> {
adContainer: <UIView: 0x104a4ee80; frame = (0 0; 390 763); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x113c25b20>; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x112bd5260>>,
companionSlots: {
},
}
[OQAdsLogger] avPlayerVideoDisplay = <IMAAVPlayerVideoDisplay: 0x113d24880>
[OQAdsLogger] pictureInPictureProxy= <IMAPictureInPictureProxy: 0x1168ea4c0>
[OQAdsLogger] VMAP payload (86479 bytes):
<?xml version='1.0' encoding='utf-8'?>
<vmap:VMAP xmlns:vmap="http://www.iab.net/vmap-1.0" version="1.0"><vmap:AdBreak breakId="cue_point--789000348" breakType="linear" timeOffset="00:29:28"><vmap:AdSource allowMultipleAds="true" followRedirects="true" id="1"><vmap:VASTAdData><VAST xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:noNamespaceSchemaLocation="vast.xsd"><Ad id="breakIntro" sequence="1"><InLine><AdSystem>VizChoice</AdSystem><AdTitle>Oqee Cine Break Intro</AdTitle><Creatives><Creative><Linear><Duration>00:00:03</Duration><MediaFiles><MediaFile delivery="progressive" width="1920" height="1080" type="video/mp4">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/free_cine_hd.mp4</MediaFile><MediaFile delivery="streaming" width="1920" height="1080" type="application/dash+xml">
https://replay-01.bzn.oqee.net/oqee-static/barkers/oqee-cine/2025-10-21/adaptative/playlist_214c3e5132137e02.mpd</MediaFile><Medi<…>
[OQAdsLogger]-[IMAAdsRequest init] [non-PiP path]
[OQAdsLogger] adDisplayContainer = <IMAAdDisplayContainer: 0x113c9bcc0> {
...
[OQAdsLogger] -> IMAAdsRequest = 0x113d24980
[OQAdsLogger] -[IMAAdsLoader requestAdsWithRequest:]
[OQAdsLogger] loader = <IMAAdsLoader: 0x101017360>
[OQAdsLogger] request = <IMAAdsRequest: 0x113d24980>
[OQAdsLogger] requestAdsWithRequest dispatched
[OQAdsLogger] -[IMAAdsManager initializeWithAdsRenderingSettings:]
[OQAdsLogger] adsManager = <IMAAdsManager: 0x113cf1770>
[OQAdsLogger] renderingSettings = (nil — default)
[OQAdsLogger] VMAP parsed, ad breaks scheduled
The app processes both Picture-in-Picture (PiP) and non-PiP variants of the VMAP.
How to Rickroll the IMA SDK
Since the adsResponse string is entirely under our control once we intercept it in initWithAdsResponse:, we can pass anything we want. Instead of instantly stripping the ads, I couldn’t resist a bit of fun. I crafted a custom VMAP to replace the pre-roll ad with a classic.
- (instancetype)initWithAdsResponse:(NSString *)adsResponse
adDisplayContainer:(id)adDisplayContainer
contentPlayhead:(id)contentPlayhead
userContext:(id)userContext {
NSString *tweakedVMAP = @"<?xml version='1.0' encoding='utf-8'?>\n"
@"<vmap:VMAP xmlns:vmap=\"http://www.iab.net/vmap-1.0\" version=\"1.0\">\n"
@" <vmap:AdBreak breakId=\"pre-roll-intro\" breakType=\"linear\" timeOffset=\"start\">\n"
@" <vmap:AdSource allowMultipleAds=\"false\" followRedirects=\"true\" id=\"1\">\n"
@" <vmap:VASTAdData>\n"
@" <VAST xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" version=\"3.0\" xsi:noNamespaceSchemaLocation=\"vast.xsd\">\n"
@" <Ad id=\"breakIntro\" sequence=\"1\">\n"
@" <InLine>\n"
@" <AdSystem>VizChoice</AdSystem>\n"
@" <AdTitle>Oqee Cine Break Intro</AdTitle>\n"
@" <Creatives>\n"
@" <Creative>\n"
@" <Linear>\n"
@" <Duration>00:00:03</Duration>\n"
@" <MediaFiles>\n"
@" <MediaFile delivery=\"progressive\" width=\"1920\" height=\"1080\" type=\"video/mp4\">https://noh.am/rick.mp4</MediaFile>\n"
@" </MediaFiles>\n"
@" </Linear>\n"
@" </Creative>\n"
@" </Creatives>\n"
@" </InLine>\n"
@" </Ad>\n"
@" </VAST>\n"
@" </vmap:VASTAdData>\n"
@" </vmap:AdSource>\n"
@" </vmap:AdBreak>\n"
@"</vmap:VMAP>\n";
NSLog(@"%@ Replaced VMAP (%lu bytes) with tweaked document", TAG, (unsigned long)adsResponse.length);
return %orig(tweakedVMAP, adDisplayContainer, contentPlayhead, userContext);
}
IMA SDK successfully playing the overridden VMAP URL.Sorry for the Rickroll ;)
The bypass
To properly nuke the ads, all we have to do is supply the IMA SDK with a syntactically valid but completely empty VMAP document. This forces the SDK to instantly resolve the ad request queue and allow the main AVPlayer content to proceed uninterrupted.
- (instancetype)initWithAdsResponse:(NSString *)adsResponse
adDisplayContainer:(id)adDisplayContainer
contentPlayhead:(id)contentPlayhead
userContext:(id)userContext {
// Replace the VMAP with an empty document
NSString *emptyVMAP = @"<?xml version='1.0' encoding='utf-8'?>"
@"<vmap:VMAP xmlns:vmap=\"http://www.iab.net/vmap-1.0\" version=\"1.0\"/>";
NSLog(@"%@ Replaced VMAP (%lu bytes) with empty document", TAG, (unsigned long)adsResponse.length);
return %orig(emptyVMAP, adDisplayContainer, contentPlayhead, userContext);
}
Once installed, there are no pre-roll ads and the timeline doesn’t show any yellow markers or interruptions.

I ported the same logic to tvOS, and it works flawlessly on the Apple TV as well.
As always, you can find the complete tweak source code on my GitHub.
That’s it :)