IPv4 jako služba aneb jak síť zbavit dual-stacku

9. 6. 2014
Doba čtení: 7 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Jsou to už dva roky, co byl oficiálně odstartován obsah dostupný protokolem IPv6. Míček je teď na straně poskytovatelů připojení. Ti se vcelku oprávněně brání, že přidávání druhého protokolu do jejich stávající infrastruktury představuje náklady, které nikdo nezaplatí. Nešlo by to nějak jinak?

Zavedení IPv6 metodou dual-stack, tedy současný provoz protokolu IPv4 a IPv6, je často vnímáno jako jediný možný model přechodu na nový protokol. I když množství obsahu dostupného pomocí IPv6 stále narůstá, rozhodně ještě několik let nebude možné považovat síť bez podpory připojení i k IPv4 zdrojům za reálně použitelnou. Na druhé straně, stále prakticky neexistují služby, které by bez IPv6 konektivity nefungovaly.

Nasazení IPv6 metodou dual-stack tak v přístupové síti znamená jen mnoho práce, která problém s tenčící se zásobou IPv4 adres nevyřeší, nedá se moc dobře kompenzovat zvýšením ceny, ani nepřinese velké množství spokojených zákazníků. Existují proto i alternativy, které mají jedno společné: síť se jednoduše přepne na IPv6 bez podpory IPv4. Podpora spojení ke zdrojům, které IPv6 nepodporují je pak řešena jako jedna ze služeb sítě. O těchto metodách je dnešní článek.

NAT64 a DNS64

Každého jistě napadne, že při tak obrovském adresním prostoru, jakým disponuje protokol IPv6, není problém do nějaké části tohoto prostoru schovat celý IPv4 prostor. Přesně na této myšlence je založen koncept NAT64 (vyslovuje se NAT-šest-čtyři – pozn. redakce). Přístupová síť podporuje pouze IPv6, při odeslání paketu do speciálního podprostoru však takový paket dojde do zařízení, které jej přeloží na běžný IPv4 paket a odešle do IPv4 internetu. Adresní podprostor takto mapovaný do IPv4 světa může být součástí adresního prostoru provozovatele, nebo může jít o vyhrazený prefix  64:ff9b::/96


Mro, CC-BY-SA Wikimedia

Princip NAT64/DNS64

K tomu, aby se zařízení namísto k IPv4 připojovala k překládanému IPv6 prefixu, slouží úprava rekurzivního DNS severu. Ten pro dotaz na IPv6 adresu serveru, který takovou adresu nemá, uměle vytvoří odpověď mapující danou adresu do překládaného prefixu.

Známé rozšíření IPvFoo dokáže správně detekovat vyhrazený prefix a označit jej červenou barvou, i když jde o IPv6 provoz

NAT64 na vlastní kůži

Technologie NAT64 je v současnosti asi nejrozšířenějším způsobem eliminace dual-stacku. V běžném produkčním prostředí je už několik let nasazena u mobilních operátorů ve Slovinsku a USA, kteří jsou v nasazování IPv6 asi nejdál. WiFi síť s podporou NAT64 se také pravidelně objevuje na technologických konferencích jako RIPE nebo FOSDEM, kde dokonce taková síť přebrala roli hlavní konferenční WiFi sítě. Pozadu nezůstal ani CZ.NIC, který na své konferenci IT 14 podobně zařízenou IPv6-only WiFi síť provozoval na jednom routeru Turris, který zároveň prováděl NAT64 i DNS64.

Máte-li IPv6 konektivitu, můžete celkem snadno vyzkoušet, do jaké míry je řešení s NAT64 použitelné, aniž byste kamkoli jezdili. Stačí k tomu využít několik veřejně dostupných instalací NAT64, které provozuje Jan Žoržgo6Lab. Vypněte IPv4 protokol a jako rekurzivní DNS server nastavte některý uvedený na odkazované stránce. Pro adresy, které nejsou dostupné nativním IPv6, budete přesměrováni na NAT64 zařízení umístěné v go6Lab ve Slovinsku, které jej dále odbaví do IPv4 internetu.

Problémy čistého NAT64

Pokud síť s NAT64 vyzkoušíte, zjistíte, že funguje téměř všechno. Nefunkční případy můžeme rozdělit do následujících kategorií:

  • Zařízení předpokládá existenci IPv4 konektivity. To je třeba problém WiFi rozhraní všech zařízení s OS Android, včetně nejnovější verze 4.4.3.
  • Aplikace otvírá pouze IPv4 soket. Tak se například chová drtivá většina aplikací pro iOS.
  • Aplikace nepoužívá DNS, ale pracuje přímo s adresními literály. Typickým příkladem je Skype.

Řešení jménem 464XLAT

K odstranění výše uvedených problémů vznikl koncept 464XLAT. Ten popisuje plnohodnotný transport IPv4 protokolu IPv6 prostředím pomocí dvou překladačů, jednoho na straně providera (PLAT), který je totožný s NAT64/DNS64, druhého na straně zákazníka (CLAT). Právě druhý jmenovaný překladač sedí v blízkosti uživatele a řeší několik posledních případů, pro které NAT64 správně nefunguje. Může být implementován například jako mezivrstva v operačním systému, která se aktivuje, kdykoli se aplikace pokusí otevřít IPv4 soket, a transparentně požadavek obslouží skutečným IPv6 soketem namířeným do vyhrazeného IPv6 rozsahu. Druhá možnost spočívá v NAT64 algoritmu obráceném „naruby“, takže sbírá z klienta IPv4 provoz, překládá jej a posílá dále směrem k PLAT. Výhodou této možnosti je, že CLAT nemusí být implementován přímo v koncovém zařízení, ale třeba v domácím routeru. Domácí síť pak může běžet na obyčejném dual-stacku.

Jediný problém, který bylo potřeba vyřešit, aby se koncept 464XLAT stal použitelným, je automatická konfigurace CLATu. Při každém připojení do sítě je potřeba zjistit, zda je v síti přítomen PLAT a pokud ano, za jakým prefixem je schovaný IPv4 svět. Celý problém poměrně zeširoka popisuje RFC 7051, jedna z implementací je pak popsána v RFC 7050. Ta používá k objevení prefixu systém DNS: Zeptá se na IPv6 adresu dobře známého DNS jména ipv4only.arpa, ke kterému jsou přiřazeny IPv4 adresy 192.0.0.170 a 192.0.0.171. Dostane-li v odpovědi nějakou IPv6 adresu, znamená to, že v síti je zapojen PLAT a analýzou IPv6 adresy v odpovědi je možné zjistit, za jaký prefix je mapován IPv4 svět.

CLATy jsou přítomny v nejnovějších verzích OS Android, které pak jsou schopny u mobilních operátorů pracovat v režimu IPv6-only, aniž by některá aplikace poznala, že síť operátora ve skutečnosti IPv4 nepřenáší. CLAT si také můžete vyzkoušet na svém počítači, stačí nainstalovat clatd. Tento skript v Perlu provede objevení PLATu podle RFC 7050 a následně nakonfiguruje a spustí NAT64 překladač TAYGA. Od té chvíle bude fungovat vše stejně jako na síti s plnohodnotnou podporou IPv4, tedy například včetně příkazů ping nebo mosh, které IPv6 nepodporují.

Dual-Stack lite

NAT64 pochopitelně není jediná možnost, jak vytlačit souběžný provoz IPv4 i IPv6 z přístupové sítě. Za zmínku například stojí mechanizmus Dual-Stack lite, v současnosti provozovaný například u německého kabelového operátora Kabel Deutschland. V tomto režimu je domácí síť za zákaznickým routerem v obyčejném dual-stack režimu. IPv4 provoz na cestě do internetu je routerem zapouzdřen a poslán tunelem prostřednictvím IPv6 k NATu providera, který jej rozbalí a obslouží.


Mro, CC-BY-SA Wikimedia

Princip DS-lite

Mapping of Adddress and Port

Všechny předchozí metody mají jedno společné: vyžadují provádění překladu adres v jádru sítě nebo v jeho blízkosti. Taková řešení se obecně špatně škálují, neboť NAT musí ke každému spojení udržovat stavové informace. Navíc se tím podkopává základní myšlenka internetových sítí, totiž princip dumb core – smart edge, tedy inteligence umístěná na okraji a přenosová síť bez zvláštních schopností. Všechny výše popsané metody (včetně nepopisované metody obyčejného NATu IPv4 do IPv4 na straně ISP) navíc trpí jistým uživatelským nekomfortem, kdy uživatelé již nejsou schopni na svém koncovém zařízení přesměrovat určité číslo portu do své domácí sítě.

Oba tyto problémy řeší obsáhlá specifikace zvaná The Address plus Port (A+P) Approach to the IPv4 Address Shortage, kterou dále vyvíjí Cisco pod názvem MAP. Tento přechodový mechanismus eliminuje potřebu pro stavový NAT v jádru sítě; zároveň však umožňuje sdílení jedné IPv4 adresy několika zákazníky. Každý zákazník má k dispozici pouze omezené množství portů transportního protokolu, které jsou k jeho přípojce směrovány bezestavově. Router u zákazníka musí být upraven tak, aby pro komunikaci se zbytkem světa použil pouze jemu přidělený rozsah portů. Zákazník má ale plnou možnost nastavit pro některý z nich přesměrování do domácí sítě a tak mít určité domácí služby přístupně i po IPv4.

Příklad, jak rozdělit adresy mezi zákazníky, je možné simulovat ve webové aplikaci. Výše uvedený rozděluje 256 IPv4 adres mezi 65536 zákazníků. Každý zákazník má k dispozici 240 portů z dané IPv4 adresy. To, kterou IPv4 adresu a který rozsah portů může zákazník využívat, je zakódováno šestnáctibitovým číslem, které je součástí IPv6 prefixu zákazníka.

Podle způsobu, jakým se IPv4 provoz přenáší v přístupové IPv6 síti, se protokol MAP dále dělí na MAP-T (Translation), který funguje obdobně jako 464XLAT a MAP-E (Encapsulation), který funguje obdobně jako DS-lite. I když protokol MAP ještě není zralý k produkčnímu nasazení, jeho síťová část je už nyní k dispozici v routerech značky Cisco. Pro zákaznickou část je zatím k dispozici několik testovacích implementací, které je možné nainstalovat do libovolného routeru s OpenWRT. Podrobněji tento koncept představoval Andrew Yourtchenko v přednášce Run your next CGN on a $20 OpenWRT. Níže se můžete podívat na demonstraci takového řešení:

Pokrok nastává pomalu

Dobrou zprávou je, že okamžikem zapnutí IPv6 u velkých poskytovatelů služeb se vývoj nezastavil. Během dvou let se povedlo dosáhnout bezproblémové funkčnosti téměř všech desktopových operačních systémů. Smutné je, že se z jejich vývoje nepoučili vývojáři mobilních platforem.

Přístupové sítě zatím stále váhají. Příklady ze zahraničí ale ukazují, že kompletní přepnutí přístupové sítě z IPv4 na IPv6 není utopií ani hudbou budoucnosti, je možné jej udělat už dnes a často i tak, že si toho zákazník ani nevšimne.

Autor článku

Ondřej Caletka vystudoval obor Telekomunikační technika na ČVUT a dnes pracuje ve vzdělávacím oddělení RIPE NCC, mezinárodní asociaci koordinující internetové sítě.

'; document.getElementById('preroll-iframe').onload = function () { setupIframe(); } prerollContainer = document.getElementsByClassName('preroll-container-iframe')[0]; } function setupIframe() { prerollDocument = document.getElementById('preroll-iframe').contentWindow.document; let el = prerollDocument.createElement('style'); prerollDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:20px;right:25px}"; videoContent = prerollDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('PREROLL sound allowed'); // setUpIMA(true); videoContent.volume = 1; videoContent.muted = false; setUpIMA(); }).catch(function () { console.log('PREROLL sound forbidden'); videoContent.volume = 0; videoContent.muted = true; setUpIMA(); }); } } function setupDimensions() { prerollWidth = Math.min(iinfoPrerollPosition.offsetWidth, 480); prerollHeight = Math.min(iinfoPrerollPosition.offsetHeight, 320); } function setUpIMA() { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Preroll advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = prerollWidth; // adsRequest.linearAdSlotHeight = prerollHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. prerollDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( prerollDocument.getElementById('adContainer'), videoContent); } function unmutePrerollAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } } function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(prerollWidth, prerollHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } function onAdEvent(adEvent) { const ad = adEvent.getAd(); console.log('Preroll event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: if (!ad.isLinear()) { videoContent.play(); } prerollDocument.getElementById('adContainer').style.width = '100%'; prerollDocument.getElementById('adContainer').style.maxWidth = '640px'; prerollDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); if (ad.isLinear()) { intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } prerollDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (prerollLastError === 303) { playYtVideo(); } break; case google.ima.AdEvent.Type.COMPLETE: if (ad.isLinear()) { clearInterval(intervalTimer); } playYtVideo(); break; } } function onAdError(adErrorEvent) { console.log(adErrorEvent.getError()); prerollLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { playYtVideo(); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoPrerollPosition.remove(); playPrerollAd(); } else { return false; } adVolume = 1; return true; } function onContentPauseRequested() { videoContent.pause(); } function onContentResumeRequested() { videoContent.play(); } function onActiveView() { if (prerollContainer) { const containerOffset = prerollContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (prerollPaused) { adsManager.resume(); prerollPaused = false; } return true; } else { if (!prerollPaused) { adsManager.pause(); prerollPaused = true; } } } return false; } function playYtVideo() { iinfoPrerollPosition.remove(); youtubeIframe.style.display = 'block'; youtubeIframe.src += '&autoplay=1&mute=1'; } }
'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »