Bitcoiny z ministerské kauzy někdo až do konce května opakovaně rozměňoval

Včera
Doba čtení: 8 minut

Sdílet

Bitcoin - ilustrace
Autor: Lupa.cz, DALL-E
Z adresy, která dotovala ministerský dar, odešlo v dalších dnech 200 BTC a ty se postupně rozpouštěly v sérii transakcí, zřejmě jako příprava na směnu.

Při minulé základní analýze bitcoinového blockchainu ve vztahu k miliardě darované v této kryptoměně Ministerstvu spravedlnosti odsouzeným překupníkem Tomášem Jiřikovským jsme si ukázali, že k převodům tu docházelo před darem samotným i po něm. Během dvou dnů několik let spící peněženka spojovaná s nechvalně známým darknetovým tržištěm Nucleus Market ožila a vydala dohromady 1511 BTC (v přepočtu 3,453 miliardy korun).

To je ale méně, než v ní podle zjištění znalce Jiřího Bergera, zachyceného screenshotem obrazovky otevřené peněženky připojeného k notářském zápisu mělo být. Notářský zápis mluví o 1561 BTC. To znamená, že 50 BTC v těch adresách, které znalec bezpečně propojil s klíči v té době otevřené peněženky, někde zůstává. A současně je to mnohem míň, než s jakou úložkou peněženky spojované s Nucleem podle blockchainových záznamů disponují. Na začátku března na nich bylo 5366 BTC.

Spící peněženka z bitcoinové kauzy obživla jen na dva dny. Miliardy z ní mizely před darem i po něm Přečtěte si také:

Spící peněženka z bitcoinové kauzy obživla jen na dva dny. Miliardy z ní mizely před darem i po něm

V některých médiích se objevilo, že v rámci převodu daru na ministerstvo pocházela ze zdrojů spojovaných s Nucleem jen část a další část přišla odjinud. Jak si dnes ukážeme, není to tak docela pravda. Všechny Bitcoiny převedené na stát byly až do 6. března součástí zdrojů spojovaných s darknetem Nukleus Market. Jen 228 BTC, které na adresu státu dorazily jako poslední v 11:29 hodin, přišly oklikou. Pocházely totiž z peněženky 22c116503b, na kterou o den dříve bylo z účtů Nuclea převedeno dohromady 546,25 BTC. Tu adresu si zapamatujme, bude hrát důležitou roli. A odpoledne po daru sem z různých jiných zdrojů (nejen Nuklea) přiteklo po kouskách dalších 100 BTC.

Je otázkou, co původce transakce vedlo k tomu, že platby na jednu a tutéž adresu rozděloval zpočátku po 100 BTC, později pak po 6 BTC, snad aby příliš vysoká suma nevzbuzovala v mempoolu, resp. v blockchainu přílišnou pozornost… 

Tentokrát ponecháme úplně stranou dar Ministerstvu spravedlnosti, ale očima blockchainové analytiky se podívejme na to, co se dělo se všemi ostatními převody v oněch dvou březnových dnech, kdy se peněženka po devíti letech probudila a začala vydávat úspory. Mluvíme o 1042 BTC, které po částech opustily peněženky Nuclea před vlastním problematickým darem a záhy po něm. Ty totiž rozhodně nezahálely ani v dubnu a dokonce ani na konci května, kdy už se jednalo o hodně horkou půdu.

Anonymita krypta má své limity

Dopředu zklameme všechny ty, kteří by očekávali, že propojíme jednotlivé adresy peněženek s konkrétními příjemci. Bitcoinový blockchain, ač jednou pro vždy zachycuje, z jaké adresy, kam a kolik jednotek kryptoměny odešlo, poskytuje velkou míru anonymitu co do identity stran dané transakce. Pokud se sama neprozradí, například tím, že nechá sepsat notářský zápis, kde bude uvedeno, v jakém čase a kolik BTC se převádí, nebo pokud se nerozhodne prostředky směnit za státní peníze, tak až na výjimky není způsob, jak ztotožnit příjemce, tím spíše, pokud má pro tento jeden účel vygenerovanou samostatnou adresu.

Výjimky z tohoto pravidla bitcoinové anonymity jsou v zásadě dvě. Jednu drží v rukou sám majitel peněženky, druhou potom burza nebo směnárna, pokud se nabyvatel kryptoměny bude pokoušet své Bitcoiny proměnit na fiat, tedy klasické peníze, ať už jde o koruny, eura, dolary nebo jakoukoliv jinou měnu.

Odhalení veškeré historie transakcí může přivodit sám držitel peněženky, pokud z neopatrnosti nebo úmyslně zveřejní svůj XPUB klíč. Řadu let se používají hierarchicky deterministické peněženky odvozené právě od XPUB (nebo YPUB či ZPUB) klíče. To znamená, že koncové adresy, které se účastní transakcí, jsou kryptografickým způsobem vytvořeny z tohoto unikátního řetězce. Prozrazením XPUB klíče získáte přehled o všech transakcích, které proběhly s odvozenými adresami.

Této vlastnosti se využívá například u e-shopů přijímajících platby v kryptu. Aby byly schopny dohledat, že jim za konkrétní službu nebo zboží přišla správná částka, vytváří ke každé faktuře unikátní adresu odvozenou od XPUB klíče. Takovou adresu musí být e-shop schopen okamžitě v případě očekávané platby vygenerovat, proto klíč musí mít uložen na serveru. Unikne-li odsud, rázem je veškeré kryptoúčetnictví daného e-shopu zcela veřejné.

K druhé výjimce při převodu na státní měnu institucionální cestou se ještě dostaneme. Teď se ale vraťme do 6. března ve 13:25 hod. odpoledne. V tu chvíli probíhá první zkušební převod 0,1 BTC na adresu 055adaf7db. Po ověření, že vše funguje, jak má, v rychlém sledu následuje pět dalších převodů (1., 2., 3., 4. a 5.). Další půl hodinu se neděje nic a ve 13:54 se postup opakuje, tentokrát na už zmíněnou peněženku 22c116503b, která figuruje v převodu daru na ministerstvo. Opět stejný modus operandi, napřed zkušební převod 0,1 BTC, následovaný pěti příkazy odeslanými ve stejném okamžiku (1., 2., 3., 4. a 5.)

Peněženka 02f00fc1a3 byla aktivní do konce května

Peněženka 02f00fc1a3 byla aktivní až do konce května 

Autor: Screenshot, Martin Drtina

Další den transakční historie začíná v 09:08, kdy probíhá schůzka Jiřikovského, jeho advokáta Kárima Titze, notáře Lubomíra Miky, náměstka ministra spravedlnosti Radomíra Daňhela a znalce Jiřího Bergera v jeho znalecké kanceláři e-Fractal na pražských Vinohradech. Realizuje se domluvený převod 468,468 BTC. Ve 12 hodin podle notářského zápisu schůzka končí a notář odchází.

Ještě předtím, v 11:56 hod., odchází na nám již dobře známou peněženku 22c116503b postupně 10, pak ještě jednou 10 a 5 BTC. A pokračuje se i po obědě. V čase 12:16 až 12:23 odejde na to samé místo celkem 9 transakcí, jednou po 7 a pak po 6 BTC (1., 2., 3., 4., 5., 6., 7., 8. a 9.).

Večerní převody vyvolávají další otazníky

Večer je transakční historie mnohem pestřejší. Za čtvrt hodiny, v čase mezi 22:04 až 22:18, odejde na různá místa vždy 6 BTC. Máme tu třikrát známou peněženku 22c116503b (1., 2. a 3.). Potom se nám tu objevuje nová adresa 070b4e7385, kam přešlo 36 BTC v šesti transakcích a zůstaly tam. To samé platí pro ce9a286740 (převedeno 30 BTC a zůstaly tam) a d2f4c3ec2c (s uloženými 18 BTC).

Dále je to adresa 3c6cd33b71, sem z Nuklea přiteklo daný večer 31 BTC, ale dalších 100 sem odevzdala 18. března nám známá peněženka 22c116503b, aby 31.3. byl zůstatek na několikrát převeden dál. Napřed sem: cd11dcbf59, následně na 367757f257, pak na e81be31b05 (93,3 BTC) a fddbffc824 (5 BTC), cabf1c5b37 (91 BTC) a 0f80be463a (2 BTC), c81cc55dc8 (86 BTC) a 375590bb01 (5 BTC), a takto to pokračuje dál dokonce i v květnových dnech. Tedy dochází k postupnému rozmělňování původní velké sumy 100 BTC až na úroveň 65 BTC.

Potom je tu další adresa 3c6cd33b71, i sem naše známá peněženka odevzdává 100 BTC a i tady se postupnými transakcemi porcuje medvěd na úroveň 65 BTC.

Rozměňování Bitcoinů

Proces postupného rozměňování 100 BTC 

Autor: Screenshot, Martin Drtina

Postupné transakce jsou přitom pro plátce nevýhodné z hlediska poplatků. Pokud by na jednotlivé účty převáděl části zůstatku najednou, v jednom příkaze, zaplatí jeden transakční poplatek, zatímco postupné převody jsou poplatkem zatížené každý zvlášť. Na druhou stranu, kdo disponuje více než pěti tisíci Bitcoiny, asi tyto deseti- nebo stokorunové provize těžařům úplně neřeší.

Rozměňování jako příprava na směnu

Zjevné rozměňování vysokých částek na menší je možné spojovat se snahou připravit tyto prostředky na směnu. Tedy pokusit se alespoň část původního bitcoinového portfolia proměnit na fiat (na některou ze státních měn). To je možné dvěma způsoby. Buď si sami najdete protistranu, která bude ochotna ke směně, a nebude jí až tak záležet na tom, jaký je původ převáděných Bitcoinů, nebo využijete služeb kryptoburz či kryptosměnáren.

Ty jsou vázány poměrně tvrdou regulací, zejména opatřením proti praní špinavých peněz (AML), a s tím souvisejícím procesem KYC (Know Your Customer). Směnárna nebo burza neotevře účet, aniž by si ověřila totožnost, a od určité limitu i bonitu žadatele a původ vkládaných prostředků. „Každý uživatel musí projít KYC ověřením a pokud si navýší měsíční limit nad 250 000 Kč, ověřujeme i původ prostředků – a to jak u kryptoměn, tak u fiat měn,“ říká CEO burzy Coinmate Roman Valihrach. Stejná pravidla se týkají obchodníků s kryptoměnami ve všech státech EU.

Prvotním prověřením klienta to ale nekončí. Prověřuje se každá jednotlivá transakce. Některé automaticky, jiné manuálně. Že by burza od státu dostávala seznam podezřelých adres, Valihrach odmítá. Přesto typově podobné zdroje Coinmate používá. „Existují mezinárodní blacklisty adres spojených například s darknetem, hacky nebo praním špinavých peněz. Tyto seznamy využívá náš systém pro automatické hodnocení rizikovosti transakcí,“ říká.

Pokud má směnárna či burza důvodné podezření, že s kryptoměnou je něco v nepořádku, transakci zablokuje a nahlásí Finančnímu analytickému úřadu (FAÚ). „Další postup závisí už na jejich rozhodnutí,“ naznačuje Valihrach.

Kromě toho burza využívá pokročilou analytiku nad blockchainem. U Coinmate je to Chainalysis, která umí trasovat pohyb kryptoměn přes více peněženek i identifikovat používání tzv. mixérů. „Mixér sám o sobě není nelegální. Ale je to červená vlajka. Značí vyšší riziko a obvykle spouští podrobnější manuální prověřování,“ dodává.

CIF25

S Bitcoiny z ministerské kauzy by si burza poradila snadno. „Pravidla platí pro všechny stejně. Nejprve proběhne automatická kontrola, která prověří, jestli adresa není spojena s podvody, hacky nebo darknetem. Pokud ano, transakce se zastaví a předává se k ručnímu prověření. Pokud je objem vyšší než 250 tisíc korun měsíčně, přidává se manuální kontrola původu prostředků, kdy uživatel musí doložit, odkud kryptoměna pochází – například výpisem z jiné burzy nebo smlouvou,“ popisuje postup šéf Coinmate a dodává: „v tomhle konkrétním případě by transakce neprošla ani jednou z kontrol. Kryptoměny bychom zmrazili a celou věc nahlásili příslušným úřadům.“

Podobná pravidla má i směnárna Anycoin. I ta by podle jednatele Marka Kyrsche takovéto příchozí Bitcoiny zablokovala na vkladové adrese. „Každý uživatel má svou oddělenou vkladovou peněženku. V případě příchozí transakce z takto označených adres jsou prostředky automaticky blokovány,“ říká. Regule AML se vztahují na všechny příchozí i odchozí transakce. „S podrobnou analýzou transakce bychom převod oznámili Finančnímu analytickému úřadu. Bitcoiny by zůstaly zmrazeny do pokynu FAÚ nebo policie,“ dodává Kyrsch.

  • Chcete mít Lupu bez bannerů?
  • Chcete dostávat speciální týdenní newsletter o zákulisí českého internetu?
  • Chcete mít k dispozici strojové přepisy podcastů?
  • Chcete získat slevu 1 000 Kč na jednu z našich konferencí?

Staňte se naším podporovatelem

Autor článku

Redaktor serveru Lupa.cz se zaměřením na telekomunikace, média, IT a právo. Dříve šéfredaktor Právního rádce a mluvčí Českého telekomunikačního úřadu.

'; 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'; } }
Upozorníme vás na články, které by vám neměly uniknout (maximálně 2x týdně).
'; 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 »