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

Timeline with ad breaks

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);
}
The 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.

Empty timeline with no ads

I ported the same logic to tvOS, and it works flawlessly on the Apple TV as well.

No ads on the Apple TV either.

As always, you can find the complete tweak source code on my GitHub.

That’s it :)