PoEtický dům: napájíme po ethernetovém kabelu

27. 4. 2016
Doba čtení: 8 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Napájení po ethernetu je elegantní způsob, jak napájet malá síťová zařízení. Standardizované řešení je bohužel pro domácnost nákladné, levné pasivní alternativy zase mohou být nebezpečné.

Použití ethernetového kabelu pro napájení malých síťových zařízení představuje ve spoustě případů výhodnou spolupráci – buď v místě instalace vůbec nemusí být elektrická zásuvka, nebo její použití přidává nepříjemnou externí závislost. Centrální napájení také umožňuje centralizovat záložní zdroj, což je důležité zejména pro kritická zařízení, jakými jsou třeba IP telefony nebo bezpečnostní kamery.

Standardizované PoE – bezpečné, ale nákladné

Když začalo být napájení po ethernetu standardizované v rámci IEEE, bylo nutné vyvinout takový standard, který bude plně slučitelný se stávajícími vlastnostmi ethernetových sítí. Výchozími požadavky tedy byly:

  • plná funkčnost pro příkon až 15 W na vzdálenost až 100 m
  • bezproblémová slučitelnost se stávajícím hardwarem
  • odolnost proti nesprávně zapojené kabeláži
  • podpora gigabitového ethernetu

Standard 802.3af a jeho pozdější rozšíření 802.3at všechny požadavky zadání splnil. Aby protlačil dostatečný výkon i tím nejdelším podporovaným kabelem, bylo nutné zvýšit použité napájecí napětí až na samou hranici malého bezpečného napětí, s nominální hodnotou 48 V. Standard definuje dva typy zařízení, PSE coby zdroj napájení a PD coby napájené zařízení.

Specifikovány jsou dva způsoby napájení PD, označené jako režimy A a B. V režimu A je napájecí napětí vedeno tzv. fantomovým kanálem po datových párech 1–2 (oranžový) a 3–6 (zelený). Napájení je zavedeno do středních odboček oddělovacích transformátorků, takže posune potenciál obou vodičů v daném páru. Díky tomu, že data jsou přenášena napětím mezi vodiči jednoho páru, stejnosměrný posun napětí obou vodičů přenos dat nijak neovlivní. V režimu B je napájení vedeno obdobným způsobem po párech 4–5 a 7–8. Tento režim je výhodnější zejména pro samostatné injektory napájení pro zařízení o rychlosti 100 Mbps nebo méně. V takovém případě se totiž tyto páry vůbec nevyužívají a není tak mezilehlý injektor nutné osazovat oddělovacími transformátorky pro vytvoření fantomového kanálu. Zajímavé je, že o volbě režimu vždy rozhoduje PSE, nikoli PD. To musí být schopno přijmout napájení oběma režimy.


Linear Technology

Vstupní část PD. Dvojice usměrňovacích můstků přijímá napájení v režimech A i B v libovolné polaritě (zdroj: datasheet obvodu LTC4267)

Úkolem PSE je především detekovat, zda bylo připojeno kompatibilní zařízení. To se pozná podle přítomnosti zatěžovacího rezistoru 25 kΩ v PD. Je-li obvod rozpojený, nebo naopak zkratovaný, má se za to, že kompatibilní zařízení připojeno není a PSE zůstává ve stavu detekování. Po detekci může následovat klasifikace, kdy PSE zvýší napájecí napětí, na což PD odpoví odběrem smluveného proudu podle příkonové třídy, do které zařízení patří. Teprve potom je vedení připojeno přímo k napájecímu zdroji a PD může začít odebírat proud až do limitu smluvené výkonnostní třídy. Pokles hodnoty odebíraného proudu pod určitou minimální hodnotu je v PSE vyhodnocen jako odpojení zařízení, čímž je napájení okamžitě vypnuto a přechází se zpět do stavu detekce.

Z popisu je zřejmé, že napájení PoE slučitelné se standardem představuje netriviální nároky na obou koncích vedení. Kromě řadičů, podporujících příslušnou signalizaci, zvyšuje cenu také použití relativně vysokého napájecího napětí, které je už mimo povolené pásmo nejběžnějších integrovaných obvodů pro spínané regulátory. Je proto potřeba použít odolnější a dražší součástky.

Pasivní PoE – levné, ale nebezpečné

Přítomnost dvou nevyužitých párů vodičů v ethernetovém kabelu pro rychlosti 10 a 100 Mbps přímo vyzývá kutily k jejich využití pro nejrůznější účely. Kromě napájení po ethernetu se objevily také rozdvojky, umožňující použít jeden kabel pro dvě nezávislé ethernetové linky či pro sdílení ethernetu a telefonního vedení.

Použití pro napájení je poměrně snadné a levné. Obvykle se oba vodiče daného páru spojí paralelně pro zvětšení jejich užitečného průřezu. Takzvaný PoE injektor připojí napájecí napětí mezi páry 4–5 (modrý) a 7–8 (hnědý) a PoE extraktor na straně zařízení napětí z těchto vodičů odbočí k napájecími zdroji zařízení. Je to tedy stejné jako dříve popsaný režim B u standardizovaného PoE.

Příklad sady pasivního injektoru a extraktoru

Injektor a extraktor je v takovém případě vlastně jen sada příslušných konektorů, takže je ji možné pořídit za několik desetikorun. Pro spoustu účelů takové napájení plně dostačuje, je však třeba být si vědom úskalí, která takové napájení přináší:

  • úbytek napětí na dlouhém ethernetovém kabelu
  • riziko zničení zařízení při použití nesprávné kabeláže
  • riziko zničení nekompatibilního zařízení, je-li omylem zapojeno na kabel, který obsahuje napájení

Zejména riziko zničení nekompatibilního zařízení je nepříjemné. Z toho důvodu bych čistě pasivní PoE nikdy nedoporučoval používat v místech, kde se dá předpokládat laické zapojování a přepojování kabelů. Pokud je to nevyhnutelné, je dobré kabely s napájením výrazně označit a opatřit varováním.

Vzpomínám si na své první setkání s pasivním PoE. Bylo to v roce 2012, kdy jsem se rozhodl pořídit připojení k internetu prostřednictvím místního Wi-Fi poskytovatele. Technik tehdy přinesl a nainstaloval klientský router na bázi MikroTik routerboardu. Když jej konfiguroval, vytáhl zánovní notebook a USB síťovou kartou s tím, že síťovou kartu v notebooku má nefunkční. Teprve později mi došlo, že asi tuším, jak k té nefunkční síťové kartě přišel – nejspíš si spletl porty na PoE injektoru. 

Co přesně se zničí

Zapojení kabelu s pasivním PoE napájením do běžného ethernetového rozhraní může dopadnout všelijak. Obecně lze říct, že bez větších následků by mělo přežít zařízení, které podporuje pouze rychlosti 10 a 100 Mbps. Taková zařízení vodiče 4–5 a 7–8 v kabelu vůbec nevyužívají, takže jsou obvykle jen zapojeny přes zakončovací rezistory na společný potenciál. Připojené napájecí napětí obvykle zakončovací rezistory přepálí, což může být doprovázeno drobným zápachem či kouřovými efekty, zařízení ale pracuje dál. Jsou i zařízení, která v zájmu úspory barevných kovů obsahují speciální varianty modulárních konektorů, ve kterých kontakty 4, 5, 7 a 8 vůbec nejsou. Taková zařízení pak logicky žádná újma nepotká.


CZ.NIC

Příklad konektoru s oddělovacími transformátory (tzv. MagJack), který není kompatibilní s PoE (zdroj: schéma zapojení routeru Turris)

U zařízení gigabitového ethernetu je situace mnohem horší. Všechny čtyři páry vodičů v kabelu jsou využívány pro obousměrný přenos dat. Na každém páru je tedy připojen transformátorek, přičemž střední odbočky všech čtyř párů jsou opět svedeny na společný potenciál. Případné napájecí napětí je tedy zkratováno a je otázkou konstrukce, která část bude tvořit nejslabší článek a přepálí se. V lepším případě bude síťová karta nadále schopná pracovat alespoň v rychlostech 10 a 100 Mbps, může se však stát, že nebude fungovat ani to.

Jiný MagJack, pro rychlost do 100 Mbps a s podporou PoE. Povšimněte oddělovacích kondenzátorů před zakončovacími rezistory. Tím jsou jednotlivé páry pro stejnosměrný proud odděleny.

Switch s pasivním PoE

Pasivní PoE napájení používám už řadu let k napájení venkovní klientské Wi-Fi jednotky značky MikroTik. Později jsem si od stejného výrobce pořídil další vnitřní Wi-Fi AP pro pokrytí odlehlých částí domu. I toto podporuje pasivní PoE napájení, takže jsem jeho napájecí zdroj mohl přesunout k centrální UPS. Počet napájecích adaptérů se začal nepříjemně zvyšovat a tak jsem začal hledat řešení, které by mi umožnilo napájení centralizovat, aniž bych musel utrácet celé jmění za standardizované PoE. Ukázalo se, že takový switch existuje v portfoliu firmy MikroTik pod názvem RB260GSP.

Jedná se o pěti portový ovladatelný gigabitový switch, který obsahuje čtyři porty s možností výstupu napájení (v režimu B). Na rozdíl od standardizovaného PoE se zde používá nižší úroveň napětí (switch je napájen zdrojem 24 V, jehož napětí se přibližně shoduje s napětím PoE výstupu), chybí také výkonové třídy. Co však nechybí, je automatická detekce připojení zátěže a odolnost proti zkratu. Stejný port je tak bez rizika možné zapojit jak do napájeného zařízení, tak i do zařízení, které napájení přijmout neumí. Díky podpoře VLAN jsem mohl tento switch použít jak pro napájení venkovního klienta, tak i pro napájení sekundárního AP, přestože má být každé zapojené v jiném segmentu sítě.

Switch nejen, že autodetekuje připojení napájeného zařízení, ale dokáže i v reálném čase měřit zatížení jednotlivých portů.

Pasivní PoE pro Raspberry Pi a IP telefon

Snadnost, s jakou se mi podařilo napájení centralizovat, stejně jako přítomnost dalších dvou napájených portů ve výše uvedeném switchi, mě vedla k myšlence napájet pomocí PoE další zařízení v domácnosti, konkrétně webkamerku postavenou z Raspberry Pi a IP telefon. Jediný problém, který bylo potřeba vyřešit, bylo přizpůsobení napájecího napětí 24 V z PoE napájecímu napětí daného zařízení, konkrétně 5 V pro Raspberry Pi a 9 V pro IP telefon. Vzpomněl jsem si tedy na nedávný článek o nočním lovu beze zbraní a zkusil použít hotové moduly zdrojů Mini-360, které jsou k mání doslova za pár desítek korun.

zabbix_tip

Technické parametry modulu Mini-360 uvádějí maximální vstupní napětí 23 V, což je zároveň přesně tolik, kolik dodává PoE switch. Abych napětí trochu snížil, předřadil jsem modulu můstkový usměrňovač. Ten zároveň ochrání zařízení proti případné chybě v zapojení kabeláže. Na modulu Mini-360 jsem pak trimrem nastavil požadované výstupní napětí. I když celé zařízení nevypdá příliš vzhledně, je plně funkční a umožňuje nahradit celkem čtyři napájecí zdroje jediným.

Snižující měnič Mini-360 s usměrňovacím můstkem, vsazený přímo do pasivního extraktoru.

Přístupový bod hAP ac lite, který kromě PoE vstupu obsahuje i jeden PoE výstup, opět s automatickou detekcí připojení kompatibilního zařízení.

Nestandardní, ale levné a bezpečné

Na popisované řešení centralizovaného napájení všech internetových věcí v domácnosti jsem přešel před necelými dvěma měsíci. Přestože jde veskrze o pasivní PoE, použití inteligentního switche umožňuje eliminovat největší nebezpečí – zničení nekompatibilních zařízení, jsou-li omylem zapojena do napájeného portu. Použití relativně vysokého napětí 24 V omezuje proudové namáhání kabelů a konektorů, přitom je však stále dostatečně nízké, aby jej bylo možné snižovat velmi levnými moduly spínaných regulátorů. Cena podobného PoE řešení je tedy pouhým zlomkem ceny řešení založeného na standardu 802.3af, komfort je přitom téměř srovnatelný.

Samostatné zdroje, které bylo možné eliminovat. Na snímku chybí ještě zdroj IP telefonu a webkamery.

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 »