├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cloudstitching ├── dash_js │ ├── index.html │ ├── player.js │ └── styles.css └── hls_js │ ├── index.html │ ├── player.js │ └── styles.css ├── dash_js └── simple │ ├── dai.css │ ├── dai.html │ └── dai.js ├── hbbtv ├── README.md ├── ads_manager.js ├── application.js ├── index.html ├── streamevent.xml ├── styles.css └── video_player.js ├── hls_js ├── advanced │ ├── dai.css │ ├── dai.html │ └── dai.js ├── dai_preroll │ ├── dai.css │ ├── dai.html │ └── dai.js └── simple │ ├── dai.css │ ├── dai.html │ └── dai.js ├── native └── simple │ ├── dai.css │ ├── dai.html │ └── dai.js └── podserving ├── dash_js ├── index.html ├── player.js └── styles.css └── hls_js ├── index.html ├── player.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleads/googleads-ima-html5-dai/6c6350bc8a612fe0b2f643e1a9848a46739cd400/.gitignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit patches 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your code patches! However, before we can take them, we have to clear a couple of legal hurdles. 6 | 7 | Fill out either the individual or corporate Contributor License Agreement. 8 | 9 | - If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an individual CLA available at http://code.google.com/legal/individual-cla-v1.0.html (Note the form at the bottom of the page which lets you sign electronically). 10 | - If you work for a company that wants to allow you to contribute your work to this client library, then you'll need to sign a corporate CLA available at http://code.google.com/legal/corporate-cla-v1.0.html. 11 | 12 | Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll add you to the official list of contributors and be able to accept your patches. 13 | 14 | ## Submitting Patches 15 | 16 | - Sign a Contributor License Agreement (see above). 17 | - Join the [Google Media Framework discussion group](http://groups.google.com/d/forum/google-media-framework). 18 | - Fork the library, make the changes and send a [pull request](https://help.github.com/articles/using-pull-requests). 19 | - We will review your patch and add comments if any changes are required. Once any issues are resolved, we'll merge your request! 20 | 21 | # If you can't become a contributor 22 | 23 | If you can't become a contributor, but want to share some code that illustrates an issue / shows how an issue may be fixed, then you can attach your changes on the issues page. We will use this code to troubleshoot the issue and fix it, but will not use this code in the library unless the steps to submit patches are done. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Google Ads DAI SDK for HTML5 2 | 3 | This project hosts samples for the 4 | [DAI SDK for HTML5](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5). 5 | 6 | ### Samples breakdown 7 | 8 | * [hls_js/simple](https://github.com/googleads/googleads-ima-html5-dai/tree/main/hls_js/simple) - 9 | Simple example using HLS.js. Supports HLS streams in 10 | [browsers that support the HLS.js javascript library](https://github.com/video-dev/hls.js/#compatibility). 11 | * [hls_js/advanced](https://github.com/googleads/googleads-ima-html5-dai/tree/main/hls_js/advanced) - 12 | Advanced example using HLS.js. Supports HLS streams in 13 | [browsers that support the HLS.js javascript library](https://github.com/video-dev/hls.js/#compatibility). 14 | * [hls_js/dai_preroll](https://github.com/googleads/googleads-ima-html5-dai/tree/main/dai_preroll) - 15 | Demonstrates using the IMA client-side SDK to request a pre-roll ad, then 16 | the DAI SDK to play a DAI stream with mid-rolls. Supports HLS streams in 17 | [browsers that support the HLS.js javascript library](https://github.com/video-dev/hls.js/#compatibility). 18 | * [native/simple](https://github.com/googleads/googleads-ima-html5-dai/tree/main/native/simple) - 19 | Simple example relying on native HLS support. Supports HLS streams in 20 | [browsers with native HLS support](https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Live_streaming_web_audio_and_video#streaming_file_format_support). 21 | * [dash_js/simple](https://github.com/googleads/googleads-ima-html5-dai/tree/main/dash_js/simple) - 22 | Simple example using DASH.js. For more information see the 23 | [DASH.js README](https://github.com/Dash-Industry-Forum/dash.js#readme). 24 | * [podserving/hls_js](https://github.com/googleads/googleads-ima-html5-dai/tree/main/podserving/hls_js) - 25 | Simple example using HLS.js with DAI SDK and Google DAI Pod Serving. For 26 | more information see the 27 | [DAI pod serving guide](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5?service=pod). 28 | * [podserving/dash_js](https://github.com/googleads/googleads-ima-html5-dai/tree/main/podserving/dash_js) - 29 | Simple example using DASH.js with DAI SDK and Google DAI Pod Serving. For 30 | more information see the 31 | [DAI pod serving guide](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5?service=pod). 32 | * [hbbtv](https://github.com/googleads/googleads-ima-html5-dai/tree/main/hbbtv) - 33 | Simple example for requesting and playing ad pods with 34 | [HbbTV](https://developer.hbbtv.org/). 35 | 36 | ### Requirements 37 | 38 | Your favorite text editor. An HTML5 compliant browser. A webserver on which to 39 | host the sample. 40 | 41 | ### More Info 42 | 43 | For more information, see the documentation at 44 | https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5. 45 | 46 | ### Announcements and Updates 47 | 48 | For API and client library updates and news, follow our 49 | [Google Ads Developers blog](http://googleadsdeveloper.blogspot.com/). 50 | 51 | Copyright 2017 Google Inc. All Rights Reserved. You may study, modify, and use 52 | this example for any purpose. Note that this example is provided "as is", 53 | WITHOUT WARRANTY of any kind either expressed or implied. 54 | -------------------------------------------------------------------------------- /cloudstitching/dash_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

IMA DAI SDK Demo with Cloud Video Stitcher API

24 |
Player type: (DASH.js)
25 | 38 | 39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /cloudstitching/dash_js/player.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // FILL IN THESE VARIABLES 18 | // For livestream requests: 19 | const LIVE_STREAM_EVENT_ID = ''; 20 | const CUSTOM_ASSET_KEY = ''; 21 | 22 | // For VOD stream requests: 23 | const VOD_CONFIG_ID = ''; 24 | const AD_TAG_URL = ''; // Not used when 'VOD_CONFIG_ID' is set. 25 | const CONTENT_SOURCE_URL = ''; // Not used when 'VOD_CONFIG_ID' is set. 26 | 27 | // For both live and VOD stream requests: 28 | const REGION = ''; 29 | const PROJECT_NUMBER = ''; 30 | const NETWORK_CODE = ''; 31 | const STREAM_FORMAT = 'dash'; 32 | // Replace 'TOKEN' with the output of gcloud auth print-access-token. 33 | const TOKEN = ''; 34 | // FILL IN THESE VARIABLES 35 | 36 | const BACKUP_STREAM = 'https://storage.googleapis.com/interactive-media-ads/' + 37 | 'media/tears-of-steel-DASH.mpd'; 38 | let dashPlayer; 39 | let streamManager; 40 | let videoElement; 41 | let adUiElement; 42 | let isAdBreak; 43 | 44 | /** 45 | * Initializes stream manager and attaches event listeners. 46 | **/ 47 | function initPlayer() { 48 | const requestButton = document.getElementById('request-stream'); 49 | const liveStreamButton = document.getElementById('livestream-request'); 50 | 51 | videoElement = document.getElementById('video'); 52 | adUiElement = document.getElementById('ad-ui'); 53 | 54 | dashPlayer = dashjs.MediaPlayer().create(); 55 | dashPlayer.initialize(videoElement); 56 | 57 | videoElement.addEventListener('pause', onStreamPause); 58 | videoElement.addEventListener('play', onStreamPlay); 59 | 60 | // Timed metadata is only used for LIVE streams. 61 | dashPlayer.on('urn:google:dai:2018', (payload) => { 62 | const mediaId = payload.event.messageData; 63 | const pts = payload.event.calculatedPresentationTime; 64 | streamManager.processMetadata('urn:google:dai:2018', mediaId, pts); 65 | }); 66 | 67 | const manifestLoadedListener = () => { 68 | console.log('Stream manifest loaded to video player. Ready to play the stream.'); 69 | // This listener must be removed, otherwise it triggers as additional 70 | // manifests are loaded. The manifest is loaded once for the content, 71 | // but additional manifests are loaded for upcoming ad breaks. 72 | dashPlayer.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, manifestLoadedListener); 73 | dashPlayer.play(); 74 | videoElement.controls = true; 75 | }; 76 | dashPlayer.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, manifestLoadedListener); 77 | 78 | requestButton.onclick = (e) => { 79 | e.preventDefault(); 80 | initiateStreamManager(); 81 | if (liveStreamButton.checked) { 82 | console.log('Requesting Cloud Stitching livestream'); 83 | requestLiveStream(); 84 | } else { 85 | console.log('Requesting Cloud Stitching VOD stream'); 86 | requestVODStream(); 87 | } 88 | }; 89 | } 90 | 91 | /** 92 | * Creates the IMA StreamManager and sets ad event listeners. 93 | */ 94 | function initiateStreamManager() { 95 | // Create a StreamManager before making the first stream request. 96 | // The StreamManager is used for this instance and subsequent stream requests. 97 | if (!streamManager) { 98 | streamManager = 99 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 100 | // Add event listeners 101 | streamManager.addEventListener( 102 | [ 103 | google.ima.dai.api.StreamEvent.Type.LOADED, 104 | google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED, 105 | google.ima.dai.api.StreamEvent.Type.ERROR, 106 | google.ima.dai.api.StreamEvent.Type.CLICK, 107 | google.ima.dai.api.StreamEvent.Type.STARTED, 108 | google.ima.dai.api.StreamEvent.Type.FIRST_QUARTILE, 109 | google.ima.dai.api.StreamEvent.Type.MIDPOINT, 110 | google.ima.dai.api.StreamEvent.Type.THIRD_QUARTILE, 111 | google.ima.dai.api.StreamEvent.Type.COMPLETE, 112 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 113 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, 114 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, 115 | google.ima.dai.api.StreamEvent.Type.PAUSED, 116 | google.ima.dai.api.StreamEvent.Type.RESUMED 117 | ], 118 | onStreamEvent, false); 119 | } 120 | } 121 | 122 | /** 123 | * Creates the video stitcher live stream request and passes it to the stream 124 | * manager. 125 | **/ 126 | function requestLiveStream() { 127 | const streamRequest = new google.ima.dai.api.VideoStitcherLiveStreamRequest(); 128 | // The Event ID for the live stream, as setup on the Video Stitcher. 129 | streamRequest.liveStreamEventId = LIVE_STREAM_EVENT_ID; 130 | // The region to use for the Video Stitcher. 131 | streamRequest.region = REGION; 132 | // The project number for the Video Stitcher. 133 | streamRequest.projectNumber = PROJECT_NUMBER; 134 | // The OAuth Token for the Video Stitcher, as detailed above. 135 | streamRequest.oAuthToken = TOKEN; 136 | // The network code for the publisher making this stream request. 137 | streamRequest.networkCode = NETWORK_CODE; 138 | // The custom asset key created during the live stream event registration 139 | streamRequest.customAssetKey = CUSTOM_ASSET_KEY; 140 | // Format should match the format of content source URL. 141 | streamRequest.format = STREAM_FORMAT; 142 | streamManager.requestStream(streamRequest); 143 | } 144 | 145 | /** 146 | * Creates the video stitcher VOD stream request and passes it to the stream 147 | * manager. 148 | **/ 149 | function requestVODStream() { 150 | const streamRequest = new google.ima.dai.api.VideoStitcherVodStreamRequest(); 151 | if (VOD_CONFIG_ID) { 152 | // The VOD config ID from you Cloud project. 153 | streamRequest.vodConfigId = VOD_CONFIG_ID; 154 | } else { 155 | // The URL string of the stream manifest for your VOD content. 156 | streamRequest.contentSourceUrl = CONTENT_SOURCE_URL; 157 | // Ad Manager URL of the ad tag. 158 | streamRequest.adTagUrl = AD_TAG_URL; 159 | } 160 | // The region to use for the Video Stitcher. 161 | streamRequest.region = REGION; 162 | // The project number for the Video Stitcher. 163 | streamRequest.projectNumber = PROJECT_NUMBER; 164 | // The OAuth Token for the Video Stitcher, as detailed above. 165 | streamRequest.oAuthToken = TOKEN; 166 | // The network code for the publisher making this stream request. 167 | streamRequest.networkCode = NETWORK_CODE; 168 | // Format should match the format of content source URL. 169 | streamRequest.format = STREAM_FORMAT; 170 | streamManager.requestStream(streamRequest); 171 | } 172 | 173 | /** 174 | * Handles stream events. 175 | * @param {!Event} e the event object. 176 | **/ 177 | function onStreamEvent(e) { 178 | switch (e.type) { 179 | case google.ima.dai.api.StreamEvent.Type.LOADED: 180 | console.log('Stream loaded'); 181 | videoElement.controls = true; 182 | loadUrl(e.getStreamData().url); 183 | break; 184 | case google.ima.dai.api.StreamEvent.Type.ERROR: 185 | console.log('Error loading stream, playing backup stream.', e.getStreamData().errorMessage); 186 | loadUrl(BACKUP_STREAM); 187 | break; 188 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 189 | console.log('Ad Break Started'); 190 | isAdBreak = true; 191 | videoElement.play(); 192 | break; 193 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 194 | console.log('Ad Break Ended'); 195 | isAdBreak = false; 196 | videoElement.controls = true; 197 | break; 198 | default: 199 | if (e.type !== google.ima.dai.api.StreamEvent.Type.AD_PROGRESS) { 200 | console.log(e.type); 201 | } 202 | break; 203 | } 204 | } 205 | 206 | /** 207 | * Loads the stream in DASH.js player. 208 | * @param {string} url The url of the stream to load. 209 | **/ 210 | function loadUrl(url) { 211 | console.log('Loading:' + url); 212 | dashPlayer.attachSource(url); 213 | } 214 | 215 | /** 216 | * video pause handler. 217 | **/ 218 | function onStreamPause() { 219 | console.log('paused'); 220 | if (isAdBreak) { 221 | videoElement.controls = true; 222 | } 223 | } 224 | 225 | /** 226 | * video play handler. 227 | **/ 228 | function onStreamPlay() { 229 | console.log('played'); 230 | if (isAdBreak) { 231 | videoElement.controls = false; 232 | } 233 | } -------------------------------------------------------------------------------- /cloudstitching/dash_js/styles.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2024 Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #video, 16 | #ad-ui { 17 | width: 640px; 18 | height: 360px; 19 | display: block; 20 | position: relative; 21 | top: 35px; 22 | left: 0; 23 | } 24 | 25 | #ad-ui { 26 | cursor: pointer; 27 | } 28 | -------------------------------------------------------------------------------- /cloudstitching/hls_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

IMA DAI SDK Demo with Cloud Video Stitcher API

24 |
Player type: (Unknown)
25 | 38 | 39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /cloudstitching/hls_js/player.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // FILL IN THESE VARIABLES 18 | // For livestream requests: 19 | const LIVE_STREAM_EVENT_ID = ''; 20 | const CUSTOM_ASSET_KEY = ''; 21 | 22 | // For VOD stream requests: 23 | const VOD_CONFIG_ID = ''; 24 | const AD_TAG_URL = ''; // Not used when 'VOD_CONFIG_ID' is set. 25 | const CONTENT_SOURCE_URL = ''; // Not used when 'VOD_CONFIG_ID' is set. 26 | 27 | // For both live and VOD stream requests: 28 | const REGION = ''; 29 | const PROJECT_NUMBER = ''; 30 | const NETWORK_CODE = ''; 31 | const STREAM_FORMAT = 'hls'; 32 | // Replace 'TOKEN' with the output of gcloud auth print-access-token. 33 | const TOKEN = ''; 34 | // FILL IN THESE VARIABLES 35 | 36 | const BACKUP_STREAM = 37 | '//storage.googleapis.com/testtopbox-public/video_content/bbb/master.m3u8'; 38 | let hls; 39 | let streamManager; 40 | let videoElement; 41 | let adUiElement; 42 | let isAdBreak; 43 | 44 | /** 45 | * Initializes stream manager and attaches event listeners. 46 | **/ 47 | function initPlayer() { 48 | const playbackMethodElement = document.getElementById('playback-method'); 49 | const requestButton = document.getElementById('request-stream'); 50 | const liveStreamButton = document.getElementById('livestream-request'); 51 | 52 | if (useNativePlayer()) { 53 | playbackMethodElement.textContent = 'Native Player'; 54 | } else { 55 | playbackMethodElement.textContent = 'HLS.js'; 56 | } 57 | 58 | videoElement = document.getElementById('video'); 59 | adUiElement = document.getElementById('ad-ui'); 60 | 61 | videoElement.addEventListener('pause', onStreamPause); 62 | videoElement.addEventListener('play', onStreamPlay); 63 | 64 | requestButton.onclick = (e) => { 65 | e.preventDefault(); 66 | initiateStreamManager(); 67 | if (liveStreamButton.checked) { 68 | console.log('Requesting Cloud Stitching livestream'); 69 | requestLiveStream(); 70 | } else { 71 | console.log('Requesting Cloud Stitching VOD stream'); 72 | requestVODStream(); 73 | } 74 | }; 75 | } 76 | 77 | /** 78 | * Checks whether the browser is running on a MacOS or iOS device, to use the 79 | * native video player instead of the HTML video player. 80 | * @return {boolean} is the native (Safari) video player supported. 81 | */ 82 | function useNativePlayer() { 83 | // this could be a more advanced check, but instead is a trivial navigator 84 | return navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && 85 | navigator.userAgent && navigator.userAgent.indexOf('CriOS') > -1 && 86 | navigator.userAgent.indexOf('FxiOS') > -1; 87 | } 88 | 89 | /** 90 | * Creates the IMA StreamManager and sets ad event listeners. 91 | */ 92 | function initiateStreamManager() { 93 | // Create a StreamManager before making the first stream request. 94 | // The StreamManager is used for this instance and subsequent stream requests. 95 | if (!streamManager) { 96 | streamManager = 97 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 98 | // Add event listeners 99 | streamManager.addEventListener( 100 | [ 101 | google.ima.dai.api.StreamEvent.Type.LOADED, 102 | google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED, 103 | google.ima.dai.api.StreamEvent.Type.ERROR, 104 | google.ima.dai.api.StreamEvent.Type.CLICK, 105 | google.ima.dai.api.StreamEvent.Type.STARTED, 106 | google.ima.dai.api.StreamEvent.Type.FIRST_QUARTILE, 107 | google.ima.dai.api.StreamEvent.Type.MIDPOINT, 108 | google.ima.dai.api.StreamEvent.Type.THIRD_QUARTILE, 109 | google.ima.dai.api.StreamEvent.Type.COMPLETE, 110 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 111 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, 112 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, 113 | google.ima.dai.api.StreamEvent.Type.PAUSED, 114 | google.ima.dai.api.StreamEvent.Type.RESUMED 115 | ], 116 | onStreamEvent, false); 117 | } 118 | } 119 | 120 | /** 121 | * Creates the video stitcher live stream request and passes it to the stream 122 | * manager. 123 | **/ 124 | function requestLiveStream() { 125 | const streamRequest = new google.ima.dai.api.VideoStitcherLiveStreamRequest(); 126 | // The Event ID for the live stream, as setup on the Video Stitcher. 127 | streamRequest.liveStreamEventId = LIVE_STREAM_EVENT_ID; 128 | // The region to use for the Video Stitcher. 129 | streamRequest.region = REGION; 130 | // The project number for the Video Stitcher. 131 | streamRequest.projectNumber = PROJECT_NUMBER; 132 | // The OAuth Token for the Video Stitcher, as detailed above. 133 | streamRequest.oAuthToken = TOKEN; 134 | // The network code for the publisher making this stream request. 135 | streamRequest.networkCode = NETWORK_CODE; 136 | // The custom asset key created during the live stream event registration 137 | streamRequest.customAssetKey = CUSTOM_ASSET_KEY; 138 | // Format should match the format of content source URL. 139 | streamRequest.format = STREAM_FORMAT; 140 | streamManager.requestStream(streamRequest); 141 | } 142 | 143 | /** 144 | * Creates the video stitcher VOD stream request and passes it to the stream 145 | * manager. 146 | **/ 147 | function requestVODStream() { 148 | const streamRequest = new google.ima.dai.api.VideoStitcherVodStreamRequest(); 149 | if (VOD_CONFIG_ID) { 150 | // The VOD config ID from you Cloud project. 151 | streamRequest.vodConfigId = VOD_CONFIG_ID; 152 | } else { 153 | // The URL string of the stream manifest for your VOD content. 154 | streamRequest.contentSourceUrl = CONTENT_SOURCE_URL; 155 | // Ad Manager URL of the ad tag. 156 | streamRequest.adTagUrl = AD_TAG_URL; 157 | } 158 | // The region to use for the Video Stitcher. 159 | streamRequest.region = REGION; 160 | // The project number for the Video Stitcher. 161 | streamRequest.projectNumber = PROJECT_NUMBER; 162 | // The OAuth Token for the Video Stitcher, as detailed above. 163 | streamRequest.oAuthToken = TOKEN; 164 | // The network code for the publisher making this stream request. 165 | streamRequest.networkCode = NETWORK_CODE; 166 | // Format should match the format of content source URL. 167 | streamRequest.format = STREAM_FORMAT; 168 | streamManager.requestStream(streamRequest); 169 | } 170 | 171 | /** 172 | * Handles stream events. 173 | * @param {!Event} e the event object. 174 | **/ 175 | function onStreamEvent(e) { 176 | switch (e.type) { 177 | case google.ima.dai.api.StreamEvent.Type.LOADED: 178 | console.log('Stream loaded'); 179 | videoElement.controls = true; 180 | loadUrl(e.getStreamData().url); 181 | break; 182 | case google.ima.dai.api.StreamEvent.Type.ERROR: 183 | console.log('Error loading stream, playing backup stream.' + e); 184 | loadUrl(BACKUP_STREAM); 185 | break; 186 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 187 | console.log('Ad Break Started'); 188 | isAdBreak = true; 189 | videoElement.play(); 190 | break; 191 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 192 | console.log('Ad Break Ended'); 193 | isAdBreak = false; 194 | videoElement.controls = true; 195 | break; 196 | default: 197 | if (e.type !== google.ima.dai.api.StreamEvent.Type.AD_PROGRESS) { 198 | console.log(e.type); 199 | } 200 | break; 201 | } 202 | } 203 | 204 | /** 205 | * Loads the stream in HLS.js 206 | * @param {string} url The url of the stream to load. 207 | **/ 208 | function loadUrl(url) { 209 | console.log('Loading:' + url); 210 | 211 | if (useNativePlayer()) { 212 | // Safari and iOS web browsers can load HLS files natively. 213 | videoElement.src = url; 214 | // listen for metadata events to pass to the streammanager 215 | videoElement.textTracks.addEventListener('addtrack', onAddTrack); 216 | console.log('Video Play'); 217 | videoElement.play(); 218 | videoElement.controls = true; 219 | } else { 220 | // clear HLS.js instance, if in use. 221 | hls?.destroy(); 222 | hls = new Hls(); 223 | hls.loadSource(url); 224 | hls.attachMedia(videoElement); 225 | 226 | // Timed metadata is only used for LIVE streams. 227 | hls.on(Hls.Events.FRAG_PARSING_METADATA, function(event, data) { 228 | if (streamManager && data) { 229 | // For each ID3 tag in the metadata, pass in the type - ID3, the 230 | // tag data (a byte array), and the presentation timestamp (PTS). 231 | data.samples.forEach(function(sample) { 232 | streamManager.processMetadata('ID3', sample.data, sample.pts); 233 | }); 234 | } 235 | }); 236 | hls.on(Hls.Events.MANIFEST_PARSED, function() { 237 | console.log('Video Play'); 238 | videoElement.play(); 239 | videoElement.controls = true; 240 | }); 241 | } 242 | } 243 | 244 | /** 245 | * video pause handler. 246 | **/ 247 | function onStreamPause() { 248 | console.log('paused'); 249 | if (isAdBreak) { 250 | videoElement.controls = true; 251 | } 252 | } 253 | 254 | /** 255 | * video play handler. 256 | **/ 257 | function onStreamPlay() { 258 | console.log('played'); 259 | if (isAdBreak) { 260 | videoElement.controls = false; 261 | } 262 | } -------------------------------------------------------------------------------- /cloudstitching/hls_js/styles.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2024 Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #video, 16 | #ad-ui { 17 | width: 640px; 18 | height: 360px; 19 | display: block; 20 | position: relative; 21 | top: 35px; 22 | left: 0; 23 | } 24 | 25 | #ad-ui { 26 | cursor: pointer; 27 | } 28 | -------------------------------------------------------------------------------- /dash_js/simple/dai.css: -------------------------------------------------------------------------------- 1 | #video, 2 | #click { 3 | width: 640px; 4 | height: 360px; 5 | position: absolute; 6 | top: 35px; 7 | left: 0; 8 | } 9 | 10 | #click { 11 | cursor: pointer; 12 | } 13 | 14 | #banner { 15 | width: 100%; 16 | height: 35px; 17 | background-color: black; 18 | color: white; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | } 23 | 24 | #play-button { 25 | position: absolute; 26 | top: 400px; 27 | left: 15px; 28 | } 29 | -------------------------------------------------------------------------------- /dash_js/simple/dai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

IMA SDK DAI Demo (DASH.JS)

10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dash_js/simple/dai.js: -------------------------------------------------------------------------------- 1 | // This stream will be played if ad-enabled playback fails. 2 | 3 | const BACKUP_STREAM = 4 | 'https://storage.googleapis.com/interactive-media-ads/media/' + 5 | 'tears-of-steel-DASH.mpd'; 6 | 7 | // Live stream asset key. 8 | // const TEST_ASSET_KEY = 'PSzZMzAkSXCmlJOWDmRj8Q'; 9 | 10 | // VOD content source and video IDs. 11 | const TEST_CONTENT_SOURCE_ID = '2559737'; 12 | const TEST_VIDEO_ID = 'tos-dash'; 13 | 14 | const NETWORK_CODE = '21775744923'; 15 | const API_KEY = null; 16 | 17 | // StreamManager which will be used to request ad-enabled streams. 18 | let streamManager; 19 | 20 | // dash.js video player. 21 | let dashPlayer; 22 | 23 | // Video element 24 | let videoElement; 25 | 26 | // Ad UI element 27 | let adUiElement; 28 | 29 | // The play/resume button 30 | let playButton; 31 | 32 | /** 33 | * Initializes the video player. 34 | */ 35 | function initPlayer() { 36 | videoElement = document.getElementById('video'); 37 | playButton = document.getElementById('play-button'); 38 | adUiElement = document.getElementById('adUi'); 39 | 40 | dashPlayer = dashjs.MediaPlayer().create(); 41 | dashPlayer.initialize(videoElement); 42 | 43 | streamManager = 44 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 45 | streamManager.addEventListener( 46 | [ 47 | google.ima.dai.api.StreamEvent.Type.LOADED, 48 | google.ima.dai.api.StreamEvent.Type.ERROR, 49 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 50 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED 51 | ], 52 | onStreamEvent, false); 53 | 54 | // Add metadata listener. Only used in LIVE streams. Timed metadata 55 | // is handled differently by different video players, and the IMA SDK provides 56 | // two ways to pass in metadata, StreamManager.processMetadata() and 57 | // StreamManager.onTimedMetadata(). 58 | // 59 | // Use StreamManager.onTimedMetadata() if your video player parses 60 | // the metadata itself. 61 | // Use StreamManager.processMetadata() if your video player provides raw 62 | // ID3 tags, as with dash.js. 63 | dashPlayer.on('urn:google:dai:2018', (payload) => { 64 | const mediaId = payload.event.messageData; 65 | const pts = payload.event.calculatedPresentationTime; 66 | streamManager.processMetadata('urn:google:dai:2018', mediaId, pts); 67 | }); 68 | 69 | const loadlistener = function () { 70 | dashPlayer.play(); 71 | // This listener must be removed, otherwise it triggers as addional 72 | // manifests are loaded. The manifest is loaded once for the content, 73 | // but additional manifests are loaded for upcoming ad breaks. 74 | dashPlayer.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, loadlistener); 75 | }; 76 | dashPlayer.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, loadlistener); 77 | 78 | videoElement.addEventListener('pause', () => { 79 | playButton.style.display = 'block'; 80 | }); 81 | 82 | playButton.addEventListener('click', initiatePlayback); 83 | } 84 | 85 | /** 86 | * Initiate stream playback. 87 | */ 88 | function initiatePlayback() { 89 | requestVODStream(TEST_CONTENT_SOURCE_ID, TEST_VIDEO_ID, NETWORK_CODE, API_KEY); 90 | // Uncomment line below and comment one above to request a LIVE stream. 91 | // requestLiveStream(TEST_ASSET_KEY, NETWORK_CODE, API_KEY); 92 | 93 | playButton.style.display = 'none'; 94 | playButton.removeEventListener('click', initiatePlayback); 95 | playButton.addEventListener('click', resumePlayback); 96 | } 97 | 98 | /** 99 | * Resume ad playback after an ad is paused. 100 | */ 101 | function resumePlayback() { 102 | videoElement.play(); 103 | playButton.style.display = 'none'; 104 | } 105 | 106 | /** 107 | * Requests a Live stream with ads. 108 | * @param {string} assetKey 109 | * @param {?string} networkCode 110 | * @param {?string} apiKey 111 | */ 112 | function requestLiveStream(assetKey, networkCode, apiKey) { 113 | const streamRequest = new google.ima.dai.api.LiveStreamRequest(); 114 | streamRequest.assetKey = assetKey; 115 | streamRequest.networkCode = networkCode; 116 | streamRequest.apiKey = apiKey; 117 | streamRequest.format = 'dash'; 118 | streamManager.requestStream(streamRequest); 119 | } 120 | 121 | /** 122 | * Requests a VOD stream with ads. 123 | * @param {string} cmsId 124 | * @param {string} videoId 125 | * @param {?string} networkCode 126 | * @param {?string} apiKey 127 | */ 128 | function requestVODStream(cmsId, videoId, networkCode, apiKey) { 129 | const streamRequest = new google.ima.dai.api.VODStreamRequest(); 130 | streamRequest.contentSourceId = cmsId; 131 | streamRequest.videoId = videoId; 132 | streamRequest.networkCode = networkCode; 133 | streamRequest.apiKey = apiKey; 134 | streamRequest.format = 'dash'; 135 | streamManager.requestStream(streamRequest); 136 | } 137 | 138 | /** 139 | * Responds to a stream event. 140 | * @param {!google.ima.dai.api.StreamEvent} e 141 | */ 142 | function onStreamEvent(e) { 143 | switch (e.type) { 144 | case google.ima.dai.api.StreamEvent.Type.LOADED: 145 | console.log('Stream loaded'); 146 | loadUrl(e.getStreamData().url); 147 | break; 148 | case google.ima.dai.api.StreamEvent.Type.ERROR: 149 | console.log('Error loading stream, playing backup stream.' + e); 150 | loadUrl(BACKUP_STREAM); 151 | break; 152 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 153 | console.log('Ad Break Started'); 154 | videoElement.controls = false; 155 | adUiElement.style.display = 'block'; 156 | break; 157 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 158 | console.log('Ad Break Ended'); 159 | videoElement.controls = true; 160 | adUiElement.style.display = 'none'; 161 | break; 162 | default: 163 | break; 164 | } 165 | } 166 | 167 | /** 168 | * Loads and plays a Url. 169 | * @param {string} url 170 | */ 171 | function loadUrl(url) { 172 | console.log('Loading:' + url); 173 | dashPlayer.attachSource(url); 174 | } 175 | -------------------------------------------------------------------------------- /hbbtv/README.md: -------------------------------------------------------------------------------- 1 | # HbbTV Linear Sample App with IMA HTML5 DAI SDK 2 | 3 | This HbbTV linear sample app demonstrates the IMA HTML5 DAI SDK integration. It 4 | uses HbbTV stream events for detecting ad breaks and 5 | [dash.js](https://github.com/Dash-Industry-Forum/dash.js/) 6 | (version 4.6.0 or later) for ad playback. This application is intended to run 7 | as an HbbTV app on a compatible device. 8 | 9 | For more details on integrating IMA SDK in your own HbbTV app, see 10 | [Get started with IMA SDK on HbbTV](https://developers.google.com/ad-manager/dynamic-ad-insertion/sdk/html5/hbbtv). 11 | 12 | ## Key Features 13 | 14 | * **Stream Events:** The app listens for HbbTV broadcast events of upcoming ad 15 | breaks. 16 | * **Preloading:** The app initiates DAI pod serving ad requests and passes the 17 | ad pod manifest to dash.js for preloading. 18 | * **Ad Break Handling:** The app listens for HbbTV events to switch from the 19 | broadcast stream to play the broadband ad break and resumes seamlessly 20 | afterward. 21 | 22 | ## Requirements 23 | 24 | * HbbTV-compliant device 25 | * dash.js version 4.6.0 or later 26 | * Web server to host the application 27 | 28 | ## Testing Environment Setup 29 | 30 | 1. **Broadcast Stream:** Prepare an audio/video stream containing custom AIT 31 | (Application Information Table) data. 32 | 2. **DVB Modulator:** Configure a DVB modulator to transmit the broadcast stream 33 | for reception by the hybrid terminal. 34 | 3. **Web Server:** Host the HbbTV application on a web server accessible by the 35 | hybrid terminal. 36 | 37 | For detailed instructions on setting up your testing environment, refer to this 38 | guide on [running an HbbTV application](https://developer.hbbtv.org/tutorials/running-a-hbbtv-application-on-a-hybrid-terminal/). 39 | 40 | ## How to Run 41 | 42 | Set the `stream_event_id` to match your networks event ID in `streamevent.xml`. 43 | The IMA team used the value `1` for testing this app. 44 | -------------------------------------------------------------------------------- /hbbtv/ads_manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // [START create_ad_manager] 18 | /** 19 | * Wraps IMA SDK ad stream manager. 20 | * @param {!VideoPlayer} videoPlayer Reference an instance of the wrapper from 21 | * video_player.js. 22 | */ 23 | var AdManager = function(videoPlayer) { 24 | this.streamData = null; 25 | this.videoPlayer = videoPlayer; 26 | // Ad UI is not supported for HBBTV, so no 'adUiElement' is passed in the 27 | // StreamManager constructor. 28 | this.streamManager = new google.ima.dai.api.StreamManager( 29 | this.videoPlayer.videoElement); 30 | this.streamManager.addEventListener( 31 | [ 32 | google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED, 33 | google.ima.dai.api.StreamEvent.Type.ERROR, 34 | google.ima.dai.api.StreamEvent.Type.CLICK, 35 | google.ima.dai.api.StreamEvent.Type.STARTED, 36 | google.ima.dai.api.StreamEvent.Type.FIRST_QUARTILE, 37 | google.ima.dai.api.StreamEvent.Type.MIDPOINT, 38 | google.ima.dai.api.StreamEvent.Type.THIRD_QUARTILE, 39 | google.ima.dai.api.StreamEvent.Type.COMPLETE, 40 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 41 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, 42 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, 43 | google.ima.dai.api.StreamEvent.Type.PAUSED, 44 | google.ima.dai.api.StreamEvent.Type.RESUMED 45 | ], 46 | this.onStreamEvent.bind(this), 47 | false); 48 | 49 | this.videoPlayer.setEmsgEventHandler(this.onEmsgEvent, this); 50 | }; 51 | // [END create_ad_manager] 52 | 53 | // [START ads_manager_request_stream] 54 | /** 55 | * Makes a pod stream request. 56 | * @param {string} networkCode The network code. 57 | * @param {string} customAssetKey The custom asset key. 58 | */ 59 | AdManager.prototype.requestStream = function(networkCode, customAssetKey) { 60 | var streamRequest = new google.ima.dai.api.PodStreamRequest(); 61 | streamRequest.networkCode = networkCode; 62 | streamRequest.customAssetKey = customAssetKey; 63 | streamRequest.format = 'dash'; 64 | debugView.log('AdsManager: make PodStreamRequest'); 65 | this.streamManager.requestStream(streamRequest); 66 | }; 67 | // [END ads_manager_request_stream] 68 | 69 | // [START ads_manager_stream_event] 70 | /** 71 | * Handles IMA playback events. 72 | * @param {!Event} event The event object. 73 | */ 74 | AdManager.prototype.onStreamEvent = function(event) { 75 | switch (event.type) { 76 | // Once the stream response data is received, generate pod manifest url 77 | // for the video stream. 78 | case google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED: 79 | debugView.log('IMA SDK: stream initialized'); 80 | this.streamData = event.getStreamData(); 81 | break; 82 | case google.ima.dai.api.StreamEvent.Type.ERROR: 83 | break; 84 | // Hide video controls while ad is playing. 85 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 86 | debugView.log('IMA SDK: ad break started'); 87 | this.adPlaying = true; 88 | this.adBreakStarted = true; 89 | break; 90 | // Show video controls when ad ends. 91 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 92 | debugView.log('IMA SDK: ad break ended'); 93 | this.adPlaying = false; 94 | this.adBreakStarted = false; 95 | break; 96 | // Update ad countdown timers. 97 | case google.ima.dai.api.StreamEvent.Type.AD_PROGRESS: 98 | break; 99 | default: 100 | debugView.log('IMA SDK: ' + event.type); 101 | break; 102 | } 103 | }; 104 | // [END ads_manager_stream_event] 105 | 106 | // [START ads_manager_emsg_event] 107 | /** 108 | * Callback on Emsg event. 109 | * Instructs IMA SDK to fire back VAST events accordingly. 110 | * @param {!Event} event The event object. 111 | */ 112 | AdManager.prototype.onEmsgEvent = function(event) { 113 | var data = event.event.messageData; 114 | var pts = event.event.calculatedPresentationTime; 115 | if ((data instanceof Uint8Array) && data.byteLength > 0) { 116 | this.streamManager.processMetadata('ID3', data, pts); 117 | } 118 | }; 119 | // [END ads_manager_emsg_event] 120 | 121 | // [START ads_manager_load_manifest] 122 | /** 123 | * Creates DAI pod url and instructs video player to load manifest. 124 | * @param {string} networkCode The network code. 125 | * @param {string} customAssetKey The custom asset key. 126 | * @param {number} podDuration The duration of the ad pod. 127 | */ 128 | AdManager.prototype.loadAdPodManifest = 129 | function(networkCode, customAssetKey, podDuration) { 130 | if (!this.streamData) { 131 | debugView.log('IMA SDK: No DAI pod session registered.'); 132 | return; 133 | } 134 | 135 | var MANIFEST_BASE_URL = 'https://dai.google.com/linear/pods/v1/dash/network/'; 136 | // Method: DASH pod manifest reference docs: 137 | // https://developers.google.com/ad-manager/dynamic-ad-insertion/api/pod-serving/reference/live#method_dash_pod_manifest 138 | var manifestUrl = MANIFEST_BASE_URL + networkCode + '/custom_asset/' + 139 | customAssetKey + '/stream/' + this.streamData.streamId + '/pod/' + 140 | this.getPodId() + '/manifest.mpd?pd=' + podDuration; 141 | this.videoPlayer.preload(manifestUrl); 142 | }; 143 | // [END ads_manager_load_manifest] 144 | 145 | /** 146 | * Helper Function to get an unused pod ID. 147 | * In production the pod ID is determined by an Early Break Notification Call. 148 | * @return {string} The ad pod ID. 149 | */ 150 | AdManager.prototype.getPodId = function() { 151 | return Math.trunc(new Date().getTime() / 60000); 152 | }; -------------------------------------------------------------------------------- /hbbtv/application.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var STREAM_EVENT_URL = 'streamevent.xml'; 18 | 19 | // More info here: 20 | // https://developer.hbbtv.org/tutorials/handling-the-broadcast-av-object/ 21 | var UNREALIZED_PLAYSTATE = 0; 22 | var CONNECTING_PLAYSTATE = 1; 23 | var PRESENTING_PLAYSTATE = 2; 24 | var STOPPED_PLAYSTATE = 3; 25 | 26 | // Ad break events 27 | var AD_BREAK_EVENT_ANNOUNCE = 'adBreakAnnounce'; 28 | var AD_BREAK_EVENT_START = 'adBreakStart'; 29 | var AD_BREAK_EVENT_END = 'adBreakEnd'; 30 | 31 | // Pod manifest request inputs. Add your own test values. 32 | var NETWORK_CODE = ''; 33 | var CUSTOM_ASSET_KEY = ''; 34 | 35 | var app = null; 36 | var debugView = null; 37 | 38 | // [START create_app] 39 | /** Main HbbTV Application. */ 40 | var HbbTVApp = function() { 41 | this.broadcastAppManager = document.getElementById('broadcast-app-manager'); 42 | this.broadcastContainer = document.getElementById('broadcast-video'); 43 | 44 | this.playState = -1; // -1 as null play state. 45 | 46 | try { 47 | this.applicationManager = 48 | this.broadcastAppManager.getOwnerApplication(document); 49 | this.applicationManager.show(); 50 | this.broadcastContainer.bindToCurrentChannel(); 51 | this.subscribedToStreamEvents = false; 52 | this.broadcastContainer.addEventListener( 53 | 'PlayStateChange', this.onPlayStateChangeEvent.bind(this)); 54 | 55 | debugView.log('HbbTVApp: App loaded'); 56 | this.videoPlayer = new VideoPlayer(); 57 | this.videoPlayer.setOnAdPodEnded(this.resumeBroadcast.bind(this)); 58 | } catch (e) { 59 | debugView.log('HbbTVApp: No HbbTV device detected.'); 60 | return; 61 | } 62 | 63 | this.adManager = new AdManager(this.videoPlayer); 64 | }; 65 | // [END create_app] 66 | 67 | /** 68 | * Listen to play state change events 69 | */ 70 | HbbTVApp.prototype.onPlayStateChangeEvent = function() { 71 | var playStateString = 72 | this.getBroadcastState(this.broadcastContainer.playState); 73 | debugView.log('onPlayStateChangeEvent event: ' + playStateString); 74 | // [START app_presenting_playstate_change] 75 | if (!this.subscribedToStreamEvents && 76 | this.broadcastContainer.playState == PRESENTING_PLAYSTATE) { 77 | this.subscribedToStreamEvents = true; 78 | this.broadcastContainer.addStreamEventListener( 79 | STREAM_EVENT_URL, 'eventItem', function(event) { 80 | this.onStreamEvent(event); 81 | }.bind(this)); 82 | debugView.log('HbbTVApp: Subscribing to stream events.'); 83 | this.adManager.requestStream(NETWORK_CODE, CUSTOM_ASSET_KEY); 84 | } 85 | // [END app_presenting_playstate_change] 86 | 87 | if (this.playState != this.broadcastContainer.playState) { 88 | debugView.log('onPlayStateChange event: ' + playStateString); 89 | this.playState = this.broadcastContainer.playState; 90 | } 91 | }; 92 | 93 | // [START app_stream_event] 94 | /** 95 | * Callback for HbbTV stream event. 96 | * @param {!Event} event Stream event payload. 97 | */ 98 | HbbTVApp.prototype.onStreamEvent = function(event) { 99 | var eventData = JSON.parse(event.text); 100 | var eventType = eventData.type; 101 | if (eventType == AD_BREAK_EVENT_ANNOUNCE) { 102 | this.onAdBreakAnnounce(eventData); 103 | } else if (eventType == AD_BREAK_EVENT_START) { 104 | this.onAdBreakStart(eventData); 105 | } else if (eventType == AD_BREAK_EVENT_END) { 106 | this.onAdBreakEnd(eventData); 107 | } 108 | }; 109 | // [END app_stream_event] 110 | 111 | /** 112 | * Returns current broadcast state. 113 | * @return {string} broadcast state. 114 | */ 115 | HbbTVApp.prototype.getBroadcastState = function() { 116 | var currentState = ''; 117 | 118 | switch (this.broadcastContainer.playState) { 119 | case UNREALIZED_PLAYSTATE: 120 | currentState = 'Unrealized'; 121 | break; 122 | case CONNECTING_PLAYSTATE: 123 | currentState = 'Connecting'; 124 | break; 125 | case PRESENTING_PLAYSTATE: 126 | currentState = 'Presenting'; 127 | break; 128 | case STOPPED_PLAYSTATE: 129 | currentState = 'Stopped'; 130 | break; 131 | default: 132 | currentState = 'Error'; 133 | } 134 | return currentState; 135 | }; 136 | 137 | // [START app_ad_break_announce] 138 | /** 139 | * Callback function on ad break announce stream event. 140 | * @param {!Event} event HbbTV stream event payload. 141 | */ 142 | HbbTVApp.prototype.onAdBreakAnnounce = function(event) { 143 | var eventType = event.type; 144 | var eventDuration = event.duration; 145 | var eventOffset = event.offset; 146 | debugView.log( 147 | 'HbbTV event: ' + eventType + ' duration: ' + eventDuration + 148 | 's offset: ' + eventOffset + 's'); 149 | this.adManager.loadAdPodManifest(NETWORK_CODE, CUSTOM_ASSET_KEY, eventDuration); 150 | }; 151 | // [END app_ad_break_announce] 152 | 153 | // [START app_ad_break_start] 154 | /** 155 | * Callback function on ad break start stream event. 156 | * @param {!Event} event HbbTV stream event payload. 157 | */ 158 | HbbTVApp.prototype.onAdBreakStart = function(event) { 159 | debugView.log('HbbTV event: ' + event.type); 160 | if (!this.videoPlayer.isPreloaded()) { 161 | debugView.log('HbbTVApp: Switch aborted. ' + 162 | 'The ad preloading buffer is insufficient.'); 163 | return; 164 | } 165 | this.stopBroadcast(); 166 | this.videoPlayer.play(); 167 | }; 168 | // [END app_ad_break_start] 169 | 170 | // [START app_ad_break_end] 171 | /** 172 | * Callback function on ad break end stream event. 173 | * @param {!Event} event HbbTV stream event payload. 174 | */ 175 | HbbTVApp.prototype.onAdBreakEnd = function(event) { 176 | debugView.log('HbbTV event: ' + event.type); 177 | this.videoPlayer.stop(); 178 | this.resumeBroadcast(); 179 | }; 180 | // [END app_ad_break_end] 181 | 182 | /** Starts broadcast stream. */ 183 | HbbTVApp.prototype.resumeBroadcast = function() { 184 | this.broadcastContainer.style.display = 'block'; 185 | try { 186 | debugView.log('HbbTVApp: Resuming broadcast'); 187 | this.broadcastContainer.bindToCurrentChannel(); 188 | } catch (e) { 189 | debugView.log('HbbTVApp: Could not resume broadcast stream'); 190 | } 191 | }; 192 | 193 | /** Stops broadcast stream. */ 194 | HbbTVApp.prototype.stopBroadcast = function() { 195 | this.broadcastContainer.style.display = 'none'; 196 | try { 197 | debugView.log('HbbTVApp: Stopping broadcast'); 198 | this.broadcastContainer.stop(); 199 | } catch (e) { 200 | debugView.log('HbbTVApp: Could not stop broadcast stream'); 201 | } 202 | }; 203 | 204 | /** Debug console to prompt console log visibly on the screen. */ 205 | var DebugConsole = function() { 206 | this.debugConsole = document.getElementById('console'); 207 | }; 208 | 209 | /** 210 | * Prompts debug message on the screen. 211 | * @param {string} message 212 | */ 213 | DebugConsole.prototype.log = function(message) { 214 | console.log(message); 215 | if (this.debugConsole) { 216 | var line = this.getTime() + ' ' + message; 217 | this.debugConsole.innerHTML = line + '
' + this.debugConsole.innerHTML; 218 | } 219 | }; 220 | 221 | /** 222 | * Prompts error message on the screen. 223 | * @param {string} message 224 | */ 225 | DebugConsole.prototype.error = function(message) { 226 | if (this.debugConsole) { 227 | var line = '' + this.getTime() + ' ' + message + 228 | '
'; 229 | this.debugConsole.innerHTML = line + this.debugConsole.innerHTML; 230 | } 231 | }; 232 | 233 | /** 234 | * Prompts error message on the screen. 235 | * @return {string} current timestamp. 236 | */ 237 | DebugConsole.prototype.getTime = function() { 238 | var d = new Date(); 239 | return ('0' + d.getHours()).slice(-2) + ':' + 240 | ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2); 241 | }; 242 | 243 | /** 244 | * Redirects console log error message to debug console. 245 | */ 246 | window.onerror = function(message, source, lineNumber) { 247 | console.error(message, source, lineNumber); 248 | debugView.error(message + ' (' + lineNumber + ')'); 249 | }; 250 | 251 | document.addEventListener('DOMContentLoaded', function() { 252 | debugView = new DebugConsole(); 253 | app = new HbbTVApp(); 254 | }); 255 | -------------------------------------------------------------------------------- /hbbtv/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | HbbTV Linear Sample App 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /hbbtv/streamevent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /hbbtv/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | 5 | #content-wrapper { 6 | position: absolute; 7 | width: 1280px; 8 | height: 720px; 9 | overflow: hidden; 10 | } 11 | 12 | #broadcast-video, 13 | #broadband-video, 14 | #ad-ui { 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | top: 0; 19 | left: 0; 20 | } 21 | 22 | #broadcast-app-manager { 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | width: 0; 27 | height: 0; 28 | width: 0; 29 | height: 0; 30 | } 31 | 32 | #console { 33 | top: 0; 34 | right: 0; 35 | width: 500px; 36 | height: 400px; 37 | position: absolute; 38 | z-index: 100; 39 | background: #FFF; 40 | color: #000; 41 | font-size: 12px; 42 | font-family: Arial; 43 | padding: 5px; 44 | overflow: hidden; 45 | } 46 | 47 | .error { 48 | color: #F00; 49 | } 50 | -------------------------------------------------------------------------------- /hbbtv/video_player.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // [START video_player_scheme_uri] 18 | var SCHEME_ID_URI = 'https://developer.apple.com/streaming/emsg-id3'; 19 | // [END video_player_scheme_uri] 20 | 21 | // [START video_player_ad_buffer] 22 | // Ads will only play with 10 or more seconds of ad loaded. 23 | var MIN_BUFFER_THRESHOLD = 10; 24 | // [END video_player_ad_buffer] 25 | 26 | // [START create_video_player] 27 | /** 28 | * Video player wrapper class to control ad creative playback with dash.js in 29 | * broadband. 30 | */ 31 | var VideoPlayer = function() { 32 | this.videoElement = document.querySelector('video'); 33 | this.broadbandWrapper = document.getElementById('broadband-wrapper'); 34 | this.player = dashjs.MediaPlayer().create(); 35 | this.onAdPodEndedCallback = null; 36 | 37 | // Function passed in VideoPlayer.prototype.setEmsgEventHandler. 38 | this.onCustomEventHandler = null; 39 | // Scope (this) passed in VideoPlayer.prototype.setEmsgEventHandler. 40 | this.customEventHandlerScope = null; 41 | 42 | // Function to remove all of player event listeners. 43 | this.cleanUpPlayerListener = null; 44 | debugView.log('Player: Creating dash.js player'); 45 | }; 46 | // [END create_video_player] 47 | 48 | // [START video_player_controls] 49 | /** Starts playback of ad stream. */ 50 | VideoPlayer.prototype.play = function() { 51 | debugView.log('Player: Start playback'); 52 | this.show(); 53 | this.player.attachView(this.videoElement); 54 | }; 55 | 56 | /** Stops ad stream playback and deconstructs player. */ 57 | VideoPlayer.prototype.stop = function() { 58 | debugView.log('Player: Request to stop player'); 59 | if (this.cleanUpPlayerListener) { 60 | this.cleanUpPlayerListener(); 61 | } 62 | this.player.reset(); 63 | this.player.attachView(null); 64 | this.player.attachSource(null); 65 | this.player = null; 66 | this.hide(); 67 | }; 68 | // [END video_player_controls] 69 | 70 | // [START video_player_set_ad_pod_ended] 71 | /** 72 | * Sets a callback function for when an ad pod has ended. 73 | * @param {!Function} callback Callback function. 74 | */ 75 | VideoPlayer.prototype.setOnAdPodEnded = function(callback) { 76 | this.onAdPodEndedCallback = callback; 77 | }; 78 | // [END video_player_set_ad_pod_ended] 79 | 80 | // [START video_player_preload] 81 | /** 82 | * Starts ad stream prefetching into Media Source Extensions (MSE) buffer. 83 | * @param {string} url manifest url for ad stream playback. 84 | */ 85 | VideoPlayer.prototype.preload = function(url) { 86 | if (!this.player) { 87 | this.player = dashjs.MediaPlayer().create(); 88 | } 89 | debugView.log('Player: init with ' + url); 90 | this.player.initialize(/* HTMLVideoElement */ null, url, /* autoplay */ true); 91 | 92 | this.player.updateSettings({ 93 | 'debug': { 94 | 'logLevel': dashjs.Debug.LOG_LEVEL_WARNING, 95 | 'dispatchEvent': true, // flip to false to hide all debug events. 96 | }, 97 | 'streaming': { 98 | 'cacheInitSegments': true, 99 | } 100 | }); 101 | this.player.preload(); 102 | this.attachPlayerListener(); 103 | debugView.log('Player: Pre-loading into MSE buffer'); 104 | }; 105 | // [END video_player_preload] 106 | 107 | /** 108 | * Controls the dash.js player's own logging in the debugging console. 109 | * @param {!Object} event dash.js log event. 110 | */ 111 | VideoPlayer.prototype.onLog = function(event) { 112 | if (event.level < 4) { 113 | debugView.log(event.message); 114 | } 115 | }; 116 | 117 | // [START video_player_attach_listeners] 118 | /** Attaches event listener for various dash.js events.*/ 119 | VideoPlayer.prototype.attachPlayerListener = function() { 120 | var playingHandler = function() { 121 | this.onAdPodPlaying(); 122 | }.bind(this); 123 | this.player.on(dashjs.MediaPlayer.events['PLAYBACK_PLAYING'], playingHandler); 124 | var endedHandler = function() { 125 | this.onAdPodEnded(); 126 | }.bind(this); 127 | this.player.on(dashjs.MediaPlayer.events['PLAYBACK_ENDED'], endedHandler); 128 | var logHandler = function(e) { 129 | this.onLog(e); 130 | }.bind(this); 131 | this.player.on(dashjs.MediaPlayer.events['LOG'], logHandler); 132 | var errorHandler = function(e) { 133 | this.onAdPodError(e); 134 | }.bind(this); 135 | this.player.on(dashjs.MediaPlayer.events['ERROR'], errorHandler); 136 | 137 | var customEventHandler = null; 138 | if (this.onCustomEventHandler) { 139 | customEventHandler = 140 | this.onCustomEventHandler.bind(this.customEventHandlerScope); 141 | this.player.on(SCHEME_ID_URI, customEventHandler); 142 | } 143 | 144 | this.cleanUpPlayerListener = function() { 145 | this.player.off( 146 | dashjs.MediaPlayer.events['PLAYBACK_PLAYING'], playingHandler); 147 | this.player.off(dashjs.MediaPlayer.events['PLAYBACK_ENDED'], endedHandler); 148 | this.player.off(dashjs.MediaPlayer.events['LOG'], logHandler); 149 | this.player.off(dashjs.MediaPlayer.events['ERROR'], errorHandler); 150 | if (customEventHandler) { 151 | this.player.off(SCHEME_ID_URI, customEventHandler); 152 | } 153 | }; 154 | }; 155 | // [END video_player_attach_listeners] 156 | 157 | // [START video_player_emsg_handler] 158 | /** 159 | * Sets emsg event handler. 160 | * @param {!Function} customEventHandler Event handler. 161 | * @param {!Object} scope JS scope in which event handler function is called. 162 | */ 163 | VideoPlayer.prototype.setEmsgEventHandler = function( 164 | customEventHandler, scope) { 165 | this.onCustomEventHandler = customEventHandler; 166 | this.customEventHandlerScope = scope; 167 | }; 168 | // [END video_player_emsg_handler] 169 | 170 | // [START video_player_event_callbacks] 171 | /** 172 | * Called when ad stream playback buffered and is playing. 173 | */ 174 | VideoPlayer.prototype.onAdPodPlaying = function() { 175 | debugView.log('Player: Ad Playback started'); 176 | }; 177 | 178 | /** 179 | * Called when ad stream playback has been completed. 180 | * Will call the restart of broadcast stream. 181 | */ 182 | VideoPlayer.prototype.onAdPodEnded = function() { 183 | debugView.log('Player: Ad Playback ended'); 184 | this.stop(); 185 | if (this.onAdPodEndedCallback) { 186 | this.onAdPodEndedCallback(); 187 | } 188 | }; 189 | 190 | /** 191 | * @param {!Event} event The error event to handle. 192 | */ 193 | VideoPlayer.prototype.onAdPodError = function(event) { 194 | debugView.log('Player: Ad Playback error from dash.js player.'); 195 | this.stop(); 196 | if (this.onAdPodEndedCallback) { 197 | this.onAdPodEndedCallback(); 198 | } 199 | }; 200 | // [END video_player_event_callbacks] 201 | 202 | // [START video_player_show_hide] 203 | /** Shows the video player. */ 204 | VideoPlayer.prototype.show = function() { 205 | debugView.log('Player: show'); 206 | this.broadbandWrapper.style.display = 'block'; 207 | }; 208 | 209 | /** Hides the video player. */ 210 | VideoPlayer.prototype.hide = function() { 211 | debugView.log('Player: hide'); 212 | this.broadbandWrapper.style.display = 'none'; 213 | }; 214 | // [END video_player_show_hide] 215 | 216 | // [START video_player_is_preloaded] 217 | /** 218 | * Checks if the ad is preloaded and ready to play. 219 | * @return {boolean} whether the ad buffer level is sufficient. 220 | */ 221 | VideoPlayer.prototype.isPreloaded = function() { 222 | var currentBufferLevel = this.player.getDashMetrics() 223 | .getCurrentBufferLevel('video', true); 224 | return currentBufferLevel >= MIN_BUFFER_THRESHOLD; 225 | }; 226 | // [END video_player_is_preloaded] 227 | -------------------------------------------------------------------------------- /hls_js/advanced/dai.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: arial, verdana, sans-serif; 3 | overflow: hidden; 4 | } 5 | 6 | header { 7 | text-align:center; 8 | width: 100%; 9 | height: 40px; 10 | font-size: 40px; 11 | font-weight: bold; 12 | margin-top: 20px; 13 | margin-bottom: 20px; 14 | } 15 | 16 | input[type="text"] { 17 | width: 250px; 18 | } 19 | 20 | .radio-group { 21 | display: inline-block; 22 | } 23 | 24 | .input-group { 25 | margin-top: 10px; 26 | } 27 | 28 | .link { 29 | color: blue; 30 | cursor: pointer; 31 | text-decoration: underline; 32 | } 33 | 34 | #vod-inputs { 35 | display: none; 36 | } 37 | 38 | #container { 39 | margin-left: auto; 40 | margin-right: auto; 41 | width: 900px; 42 | } 43 | 44 | #video-player { 45 | position: relative; 46 | background-color: #000; 47 | border-radius: 5px; 48 | box-shadow: 0px 0px 20px rgba(50, 50, 50, 0.95); 49 | border: 2px #ccc solid; 50 | width: 640px; 51 | height: 360px; 52 | margin-left: auto; 53 | margin-right: auto; 54 | margin-top: 20px; 55 | } 56 | 57 | #content { 58 | overflow: hidden; 59 | background-color: black; 60 | } 61 | 62 | #content, #ad-ui { 63 | position: absolute; 64 | top: 0px; 65 | left: 0px; 66 | width: 640px; 67 | height: 360px; 68 | } 69 | 70 | #input-wrapper { 71 | vertical-align: top; 72 | } 73 | 74 | #input-wrapper, #buttons { 75 | width: 800px; 76 | height: auto; 77 | margin-left: auto; 78 | margin-right: auto; 79 | padding-top: 10px; 80 | padding-left: 20px; 81 | padding-right: 20px; 82 | text-align: center; 83 | } 84 | 85 | #progress { 86 | height: 20px; 87 | margin-left: auto; 88 | margin-right: auto; 89 | text-align: center; 90 | padding: 10px; 91 | } 92 | 93 | .button { 94 | margin: 0 auto; 95 | display: inline; 96 | vertical-align: top; 97 | height: 60px; 98 | padding: 0; 99 | font-size: 22px; 100 | color: white; 101 | text-align: center; 102 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); 103 | background: #2c3e50; 104 | border: 0; 105 | border-bottom: 2px solid #22303f; 106 | cursor: pointer; 107 | -webkit-box-shadow: inset 0 -2px #22303f; 108 | box-shadow: inset 0 -2px #22303f; 109 | } 110 | 111 | #play-button { 112 | width: 150px; 113 | } 114 | 115 | #bookmark-button { 116 | width: 300px; 117 | } 118 | 119 | #companion { 120 | width: 728px; 121 | height: 90px; 122 | margin: auto; 123 | margin-top: 10px; 124 | } 125 | -------------------------------------------------------------------------------- /hls_js/advanced/dai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
IMA SDK DAI Demo (HLS.JS)
11 | 12 |
13 | Stream type: 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /hls_js/advanced/dai.js: -------------------------------------------------------------------------------- 1 | // This stream will be played if ad-enabled playback fails. 2 | const BACKUP_STREAM = 3 | 'http://storage.googleapis.com/testtopbox-public/video_content/bbb/' + 4 | 'master.m3u8'; 5 | 6 | // Live stream asset key. 7 | const TEST_ASSET_KEY = 'c-rArva4ShKVIAkNfy6HUQ'; 8 | 9 | // VOD content source and video IDs. 10 | const TEST_CONTENT_SOURCE_ID = '2548831'; 11 | const TEST_VIDEO_ID = 'tears-of-steel'; 12 | 13 | const NETWORK_CODE = '21775744923'; 14 | 15 | // StreamManager which will be used to request ad-enabled streams. 16 | let streamManager; 17 | 18 | // hls.js video player. 19 | const hls = new Hls({autoStartLoad: false}); 20 | 21 | // Radio button for Live Stream. 22 | let liveRadio; 23 | 24 | // Radio button for VOD stream. 25 | let vodRadio; 26 | 27 | // Live sample fake link. 28 | let liveFakeLink; 29 | 30 | // VOD sample fake link. 31 | let vodFakeLink; 32 | 33 | // Wrapper for live input fields. 34 | let liveInputs; 35 | 36 | // Wrapper for VOD input fields. 37 | let vodInputs; 38 | 39 | // Text box with asset key. 40 | let assetKeyInput; 41 | 42 | // Text box with CMS ID. 43 | let cmsIdInput; 44 | 45 | // Text box with Video ID. 46 | let videoIdInput; 47 | 48 | // Text box with network code. 49 | let networkCodeInput; 50 | 51 | // Text box with API key. 52 | let apiKeyInput; 53 | 54 | // Video element. 55 | let videoElement; 56 | 57 | // Play button. 58 | let playButton; 59 | 60 | // Button to save bookmark to URL. 61 | let bookmarkButton; 62 | 63 | // Companion ad div. 64 | let companionDiv; 65 | 66 | // Div showing current ad progress. 67 | let progressDiv; 68 | 69 | // Ad UI div. 70 | let adUiDiv; 71 | 72 | // Flag tracking if we are currently in snapback mode or not. 73 | let isSnapback; 74 | 75 | // Time to seek to after an ad if that ad was played as the result of snapback. 76 | let snapForwardTime; 77 | 78 | // Content time for stream start if it's bookmarked. 79 | let bookmarkTime; 80 | 81 | // Whether we are currently playing a live stream or a VOD stream 82 | let isLiveStream; 83 | 84 | // Whether the stream is currently in an ad break. 85 | let isAdBreak; 86 | 87 | /** 88 | * Initializes the page. 89 | */ 90 | function initPage() { 91 | initUI(); 92 | initPlayer(); 93 | } 94 | 95 | /** 96 | * Initializes the UI. 97 | */ 98 | function initUI() { 99 | liveRadio = document.getElementById('live-radio'); 100 | vodRadio = document.getElementById('vod-radio'); 101 | liveFakeLink = document.getElementById('sample-live-link'); 102 | vodFakeLink = document.getElementById('sample-vod-link'); 103 | liveInputs = document.getElementById('live-inputs'); 104 | vodInputs = document.getElementById('vod-inputs'); 105 | assetKeyInput = document.getElementById('asset-key'); 106 | cmsIdInput = document.getElementById('cms-id'); 107 | videoIdInput = document.getElementById('video-id'); 108 | networkCodeInput = document.getElementById('network-code'); 109 | apiKeyInput = document.getElementById('api-key'); 110 | 111 | liveRadio.addEventListener('click', onLiveRadioClick); 112 | 113 | vodRadio.addEventListener('click', onVODRadioClick); 114 | 115 | liveFakeLink.addEventListener('click', () => { 116 | onLiveRadioClick(); 117 | assetKeyInput.value = TEST_ASSET_KEY; 118 | networkCodeInput.value = NETWORK_CODE; 119 | }); 120 | 121 | vodFakeLink.addEventListener('click', () => { 122 | onVODRadioClick(); 123 | cmsIdInput.value = TEST_CONTENT_SOURCE_ID; 124 | videoIdInput.value = TEST_VIDEO_ID; 125 | networkCodeInput.value = NETWORK_CODE; 126 | }); 127 | } 128 | 129 | /** 130 | * Initializes the video player. 131 | */ 132 | function initPlayer() { 133 | videoElement = document.getElementById('content'); 134 | playButton = document.getElementById('play-button'); 135 | bookmarkButton = document.getElementById('bookmark-button'); 136 | adUiDiv = document.getElementById('ad-ui'); 137 | progressDiv = document.getElementById('progress'); 138 | companionDiv = document.getElementById('companion'); 139 | 140 | const queryParams = getQueryParams(); 141 | bookmarkTime = parseInt(queryParams['bookmark']) || null; 142 | 143 | videoElement.addEventListener('seeked', onSeekEnd); 144 | videoElement.addEventListener('pause', onStreamPause); 145 | videoElement.addEventListener('play', onStreamPlay); 146 | 147 | streamManager = new google.ima.dai.api.StreamManager(videoElement, adUiDiv); 148 | streamManager.addEventListener( 149 | google.ima.dai.api.StreamEvent.Type.LOADED, onStreamLoaded, false); 150 | streamManager.addEventListener( 151 | google.ima.dai.api.StreamEvent.Type.ERROR, onStreamError, false); 152 | streamManager.addEventListener( 153 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, onAdProgress, false); 154 | streamManager.addEventListener( 155 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, onAdBreakStarted, 156 | false); 157 | streamManager.addEventListener( 158 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, onAdBreakEnded, 159 | false); 160 | streamManager.addEventListener( 161 | google.ima.dai.api.StreamEvent.Type.STARTED, onAdStarted, false); 162 | 163 | hls.on(Hls.Events.FRAG_PARSING_METADATA, function(event, data) { 164 | if (streamManager && data) { 165 | // For each ID3 tag in our metadata, we pass in the type - ID3, the 166 | // tag data (a byte array), and the presentation timestamp (PTS). 167 | data.samples.forEach(function(sample) { 168 | streamManager.processMetadata('ID3', sample.data, sample.pts); 169 | }); 170 | } 171 | }); 172 | 173 | playButton.addEventListener('click', onPlayButtonClick); 174 | bookmarkButton.addEventListener('click', onBookmarkButtonClick); 175 | } 176 | 177 | /** 178 | * Displays the live inputs and hides the VOD inputs. 179 | */ 180 | function onLiveRadioClick() { 181 | vodInputs.style.display = 'none'; 182 | liveInputs.style.display = 'block'; 183 | } 184 | 185 | /** 186 | * Displays the VOD inputs and hides the live inputs. 187 | */ 188 | function onVODRadioClick() { 189 | liveInputs.style.display = 'none'; 190 | vodInputs.style.display = 'block'; 191 | } 192 | 193 | /** 194 | * Returns a dictionary of key-value pairs from a GET query string. 195 | * @return {!Object} Key-value dictionary for keys and values in provided query 196 | * string. 197 | */ 198 | function getQueryParams() { 199 | const returnVal = {}; 200 | const pairs = location.search.substring(1).split('&'); 201 | for (let i = 0; i < pairs.length; i++) { 202 | const pair = pairs[i].split('='); 203 | returnVal[pair[0]] = decodeURIComponent(pair[1]); 204 | } 205 | return returnVal; 206 | } 207 | 208 | /** 209 | * Handles play button clicks by requesting a stream. Also removes itself so we 210 | * don't request more streams on subsequent clicks. 211 | */ 212 | function onPlayButtonClick() { 213 | if (liveRadio.checked) { 214 | requestLiveStream(); 215 | } else { 216 | requestVODStream(); 217 | } 218 | } 219 | 220 | /** 221 | * Gets the current bookmark time and saves it to a URL param. 222 | */ 223 | function onBookmarkButtonClick() { 224 | // Handles player not ready or current time = 0 225 | if (!videoElement.currentTime) { 226 | alert( 227 | 'Error: could not get current time of video element, or current time is 0'); 228 | return; 229 | } 230 | if (isLiveStream) { 231 | alert('Error: this functionality only works for VOD streams'); 232 | } 233 | const bookmarkTime = Math.floor( 234 | streamManager.contentTimeForStreamTime(videoElement.currentTime)); 235 | history.pushState(null, null, 'dai.html?bookmark=' + bookmarkTime); 236 | } 237 | 238 | /** 239 | * Requests a Live stream with ads. 240 | */ 241 | function requestLiveStream() { 242 | isLiveStream = true; 243 | const streamRequest = new google.ima.dai.api.LiveStreamRequest(); 244 | streamRequest.assetKey = assetKeyInput.value; 245 | streamRequest.networkCode = networkCodeInput.value; 246 | streamRequest.apiKey = apiKeyInput.value; 247 | streamManager.requestStream(streamRequest); 248 | } 249 | 250 | /** 251 | * Requests a VOD stream with ads. 252 | */ 253 | function requestVODStream() { 254 | isLiveStream = false; 255 | const streamRequest = new google.ima.dai.api.VODStreamRequest(); 256 | streamRequest.contentSourceId = cmsIdInput.value; 257 | streamRequest.videoId = videoIdInput.value; 258 | streamRequest.networkCode = networkCodeInput.value; 259 | streamRequest.apiKey = apiKeyInput.value; 260 | streamManager.requestStream(streamRequest); 261 | } 262 | 263 | /** 264 | * Loads the stream. 265 | * @param {!google.ima.dai.api.StreamEvent} e StreamEvent fired when stream is 266 | * loaded. 267 | */ 268 | function onStreamLoaded(e) { 269 | console.log('Stream loaded'); 270 | loadUrl(e.getStreamData().url); 271 | } 272 | 273 | /** 274 | * Handles stream errors. Plays backup content. 275 | * @param {!google.ima.dai.api.StreamEvent} e StreamEvent fired on stream error. 276 | */ 277 | function onStreamError(e) { 278 | console.log('Error loading stream, playing backup stream.' + e); 279 | loadUrl(BACKUP_STREAM); 280 | } 281 | 282 | /** 283 | * Updates the progress div. 284 | * @param {!google.ima.dai.api.StreamEvent} e StreamEvent fired when ad 285 | * progresses. 286 | */ 287 | function onAdProgress(e) { 288 | const adProgressData = e.getStreamData().adProgressData; 289 | const currentAdNum = adProgressData.adPosition; 290 | const totalAds = adProgressData.totalAds; 291 | const currentTime = adProgressData.currentTime; 292 | const duration = adProgressData.duration; 293 | const remainingTime = Math.floor(duration - currentTime); 294 | progressDiv.innerHTML = 295 | 'Ad (' + currentAdNum + ' of ' + totalAds + ') ' + remainingTime + 's'; 296 | } 297 | 298 | /** 299 | * Handles ad break started. 300 | * @param {!google.ima.dai.api.StreamEvent} e StreamEvent fired for ad break 301 | * start. 302 | */ 303 | function onAdBreakStarted(e) { 304 | console.log('Ad Break Started'); 305 | isAdBreak = true; 306 | videoElement.controls = false; 307 | adUiDiv.style.display = 'block'; 308 | // Fixes an issue where slow-seeking into an ad causes the player to get stuck 309 | // in a paused state. 310 | videoElement.play(); 311 | } 312 | 313 | /** 314 | * Handles ad break ended. 315 | * @param {!google.ima.dai.api.StreamEvent} e Stream event fired for ad break 316 | * end. 317 | */ 318 | function onAdBreakEnded(e) { 319 | console.log('Ad Break Ended'); 320 | isAdBreak = false; 321 | videoElement.controls = true; 322 | adUiDiv.style.display = 'none'; 323 | if (snapForwardTime && snapForwardTime > videoElement.currentTime) { 324 | videoElement.currentTime = snapForwardTime; 325 | snapForwardTime = null; 326 | } 327 | progressDiv.textContent = ''; 328 | } 329 | 330 | /** 331 | * Handles ad started and displays companion ad, if any. 332 | */ 333 | function onAdStarted(e) { 334 | const companionAds = e.getAd().getCompanionAds(); 335 | for (let i = 0; i < companionAds.length; i++) { 336 | const companionAd = companionAds[i]; 337 | if (companionAd.getWidth() == 728 && companionAd.getHeight() == 90) { 338 | companionDiv.innerHTML = companionAd.getContent(); 339 | } 340 | } 341 | } 342 | 343 | /** 344 | * Loads and plays a Url. 345 | * @param {string} url 346 | */ 347 | function loadUrl(url) { 348 | console.log('Loading:' + url); 349 | hls.on(Hls.Events.MANIFEST_PARSED, () => { 350 | console.log('Video Play'); 351 | let startTime = 0; 352 | if (bookmarkTime) { 353 | startTime = streamManager.streamTimeForContentTime(bookmarkTime); 354 | // Seeking on load will trigger the onSeekEnd event, so treat this seek as 355 | // if it's snapback. Without this, resuming at a bookmark will kick you 356 | // back to the ad before the bookmark. 357 | isSnapback = true; 358 | } 359 | hls.startLoad(startTime); 360 | videoElement.addEventListener('loadedmetadata', () => { 361 | videoElement.play(); 362 | }); 363 | }); 364 | hls.loadSource(url); 365 | hls.attachMedia(videoElement); 366 | videoElement.controls = true; 367 | } 368 | 369 | /** 370 | * Takes the current video time and snaps to the previous ad break if it was not 371 | * played. 372 | */ 373 | function onSeekEnd() { 374 | if (isLiveStream) { 375 | return; 376 | } 377 | if (isSnapback) { 378 | isSnapback = false; 379 | return; 380 | } 381 | const currentTime = videoElement.currentTime; 382 | const previousCuePoint = 383 | streamManager.previousCuePointForStreamTime(currentTime); 384 | if (previousCuePoint && !previousCuePoint.played) { 385 | console.log( 386 | 'Seeking back to ' + previousCuePoint.start + ' and will return to ' + 387 | currentTime); 388 | isSnapback = true; 389 | snapForwardTime = currentTime; 390 | videoElement.currentTime = previousCuePoint.start; 391 | } 392 | } 393 | 394 | /** 395 | * Shows the video controls so users can resume after stream is paused. 396 | */ 397 | function onStreamPause() { 398 | console.log('paused'); 399 | if (isAdBreak) { 400 | videoElement.controls = true; 401 | adUiDiv.style.display = 'none'; 402 | } 403 | } 404 | 405 | /** 406 | * Hides the video controls if resumed during an ad break. 407 | */ 408 | function onStreamPlay() { 409 | console.log('played'); 410 | if (isAdBreak) { 411 | videoElement.controls = false; 412 | adUiDiv.style.display = 'block'; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /hls_js/dai_preroll/dai.css: -------------------------------------------------------------------------------- 1 | #video, 2 | #click { 3 | width: 640px; 4 | height: 360px; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | } 9 | 10 | #click { 11 | cursor: pointer; 12 | } 13 | 14 | #banner { 15 | width: 100%; 16 | height: 35px; 17 | background-color: black; 18 | color: white; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | } 23 | 24 | #content, 25 | #adContainer { 26 | position: absolute; 27 | top: 35px; 28 | left: 0; 29 | width: 640px; 30 | height: 360px; 31 | } 32 | 33 | #play-button { 34 | position: absolute; 35 | top: 400px; 36 | left: 15px; 37 | } 38 | -------------------------------------------------------------------------------- /hls_js/dai_preroll/dai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

IMA SDK DAI Preroll Demo

11 |
12 | 13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /hls_js/dai_preroll/dai.js: -------------------------------------------------------------------------------- 1 | // This sample plays a client-side preroll followed by a DAI live stream. It is 2 | // used to test the functionality of both the client side and the DAI SDK on 3 | // the same page. 4 | 5 | // This stream will be played if ad-enabled playback fails. 6 | 7 | const BACKUP_STREAM = 8 | 'http://storage.googleapis.com/testtopbox-public/video_content/bbb/' + 9 | 'master.m3u8'; 10 | 11 | // Live stream asset key. 12 | const TEST_ASSET_KEY = 'c-rArva4ShKVIAkNfy6HUQ'; 13 | 14 | // Preroll ad tag 15 | const TEST_AD_TAG = 'https://pubads.g.doubleclick.net/gampad/ads?' + 16 | 'iu=/21775744923/external/single_ad_samples&sz=640x480&' + 17 | 'cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&' + 18 | 'output=vast&unviewed_position_start=1&env=vp&impl=s&correlator='; 19 | 20 | const NETWORK_CODE = '21775744923'; 21 | const API_KEY = null; 22 | 23 | // StreamManager which will be used to request ad-enabled streams. 24 | let streamManager; 25 | 26 | // Used for playback of the preroll ad using the client side SDK. 27 | let adsLoader; 28 | let adDisplayContainer; 29 | let adsManager; 30 | 31 | // hls.js video player 32 | const hls = new Hls(); 33 | 34 | // Video element 35 | let videoElement; 36 | 37 | // Ad UI element 38 | let adUiElement; 39 | 40 | // Whether the stream is currently in an ad break. 41 | let isAdBreak; 42 | 43 | // The play/resume button 44 | let playButton; 45 | 46 | /** 47 | * Initializes the video player. 48 | */ 49 | function initPlayer() { 50 | videoElement = document.getElementById('video'); 51 | playButton = document.getElementById('play-button'); 52 | adUiElement = document.getElementById('adUi'); 53 | 54 | videoElement.addEventListener('pause', onStreamPause); 55 | videoElement.addEventListener('play', onStreamPlay); 56 | 57 | streamManager = 58 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 59 | streamManager.addEventListener( 60 | [ 61 | google.ima.dai.api.StreamEvent.Type.LOADED, 62 | google.ima.dai.api.StreamEvent.Type.ERROR, 63 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 64 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED 65 | ], 66 | onStreamEvent, false); 67 | 68 | // Client side ads setup. 69 | adDisplayContainer = new google.ima.AdDisplayContainer( 70 | document.getElementById('adContainer'), videoElement); 71 | // Must be done as the result of a user action on mobile 72 | adDisplayContainer.initialize(); 73 | adsLoader = new google.ima.AdsLoader(adDisplayContainer); 74 | adsLoader.addEventListener( 75 | google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, 76 | onAdsManagerLoaded, false); 77 | adsLoader.addEventListener( 78 | google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); 79 | 80 | // Add metadata listener. Only used in LIVE streams. Timed metadata 81 | // is handled differently by different video players, and the IMA SDK provides 82 | // two ways to pass in metadata, StreamManager.processMetadata() and 83 | // StreamManager.onTimedMetadata(). 84 | // 85 | // Use StreamManager.onTimedMetadata() if your video player parses 86 | // the metadata itself. 87 | // Use StreamManager.processMetadata() if your video player provides raw 88 | // ID3 tags, as with hls.js. 89 | hls.on(Hls.Events.FRAG_PARSING_METADATA, function(event, data) { 90 | if (streamManager && data) { 91 | // For each ID3 tag in our metadata, we pass in the type - ID3, the 92 | // tag data (a byte array), and the presentation timestamp (PTS). 93 | data.samples.forEach(function(sample) { 94 | streamManager.processMetadata('ID3', sample.data, sample.pts); 95 | }); 96 | } 97 | }); 98 | 99 | playButton.addEventListener('click', initiatePlayback); 100 | } 101 | 102 | /** 103 | * Initiate stream playback. 104 | */ 105 | function initiatePlayback() { 106 | requestPreroll(TEST_AD_TAG); 107 | playButton.removeEventListener('click', initiatePlayback); 108 | playButton.style.display = 'none'; 109 | } 110 | 111 | /** 112 | * Initiate pre-roll playback after ad click-through. 113 | */ 114 | function resumePrerollPlayback() { 115 | adsManager.resume(); 116 | playButton.removeEventListener('click', resumePrerollPlayback); 117 | playButton.style.display = 'none'; 118 | } 119 | 120 | /** 121 | * Handles an ad error (client side ads). 122 | * @param {!google.ima.dai.api.AdErrorEvent} adErrorEvent 123 | */ 124 | function onAdError(adErrorEvent) { 125 | console.log(adErrorEvent.getError()); 126 | if (adsManager) { 127 | adsManager.destroy(); 128 | } 129 | } 130 | 131 | /** 132 | * Handles the adsManagerLoaded event (client side ads). 133 | * @param {!google.ima.dai.api.AdsManagerLoadedEvent} adsManagerLoadedEvent 134 | */ 135 | function onAdsManagerLoaded(adsManagerLoadedEvent) { 136 | adsManager = adsManagerLoadedEvent.getAdsManager(videoElement); 137 | adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); 138 | adsManager.addEventListener( 139 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, function(e) { 140 | console.log('Content pause requested.'); 141 | }); 142 | adsManager.addEventListener( 143 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, function(e) { 144 | console.log('Content resume requested.'); 145 | requestLiveStream(TEST_ASSET_KEY, NETWORK_CODE, API_KEY); 146 | }); 147 | adsManager.addEventListener(google.ima.AdEvent.Type.PAUSED, function(e) { 148 | console.log('Preroll paused.'); 149 | playButton.addEventListener('click', resumePrerollPlayback); 150 | playButton.style.display = 'block'; 151 | }); 152 | adsManager.addEventListener( 153 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED, function(e) { 154 | console.log('All pre-roll ads completed.'); 155 | adDisplayContainer.destroy(); 156 | document.getElementById('adContainer').style.display = 'none'; 157 | }); 158 | 159 | try { 160 | adsManager.init(640, 360); 161 | adsManager.start(); 162 | } catch (adError) { 163 | // An error may be thrown if there was a problem with the VAST response. 164 | } 165 | } 166 | 167 | /** 168 | * Requests a preroll ad using the client side SDK. 169 | * @param {string} adTagUrl 170 | */ 171 | function requestPreroll(adTagUrl) { 172 | const adsRequest = new google.ima.AdsRequest(); 173 | adsRequest.adTagUrl = adTagUrl; 174 | adsRequest.linearAdSlotWidth = 640; 175 | adsRequest.linearAdSlotHeight = 400; 176 | adsLoader.requestAds(adsRequest); 177 | } 178 | 179 | /** 180 | * Requests a Live stream with ads. 181 | * @param {string} assetKey 182 | * @param {?string} networkCode 183 | * @param {?string} apiKey 184 | */ 185 | function requestLiveStream(assetKey, networkCode, apiKey) { 186 | const streamRequest = new google.ima.dai.api.LiveStreamRequest(); 187 | streamRequest.assetKey = assetKey; 188 | streamRequest.networkCode = networkCode; 189 | streamRequest.apiKey = apiKey; 190 | streamManager.requestStream(streamRequest); 191 | } 192 | 193 | /** 194 | * Responds to a stream event. 195 | * @param {!google.ima.dai.api.StreamEvent} e 196 | */ 197 | function onStreamEvent(e) { 198 | switch (e.type) { 199 | case google.ima.dai.api.StreamEvent.Type.LOADED: 200 | console.log('Stream loaded'); 201 | loadUrl(e.getStreamData().url); 202 | break; 203 | case google.ima.dai.api.StreamEvent.Type.ERROR: 204 | console.log('Error loading stream, playing backup stream.' + e); 205 | loadUrl(BACKUP_STREAM); 206 | break; 207 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 208 | console.log('Ad Break Started'); 209 | isAdBreak = true; 210 | videoElement.controls = false; 211 | adUiElement.style.display = 'block'; 212 | break; 213 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 214 | console.log('Ad Break Ended'); 215 | isAdBreak = false; 216 | videoElement.controls = true; 217 | adUiElement.style.display = 'none'; 218 | break; 219 | default: 220 | break; 221 | } 222 | } 223 | 224 | /** 225 | * Loads and plays a Url. 226 | * @param {string} url 227 | */ 228 | function loadUrl(url) { 229 | console.log('Loading:' + url); 230 | hls.loadSource(url); 231 | hls.attachMedia(videoElement); 232 | hls.on(Hls.Events.MANIFEST_PARSED, function() { 233 | console.log('Video Play'); 234 | videoElement.play(); 235 | }); 236 | } 237 | 238 | /** 239 | * Shows the video controls so users can resume after stream is paused. 240 | */ 241 | function onStreamPause() { 242 | console.log('paused'); 243 | if (isAdBreak) { 244 | videoElement.controls = true; 245 | adUiElement.style.display = 'none'; 246 | } 247 | } 248 | 249 | /** 250 | * Hides the video controls if resumed during an ad break. 251 | */ 252 | function onStreamPlay() { 253 | console.log('played'); 254 | if (isAdBreak) { 255 | videoElement.controls = false; 256 | adUiElement.style.display = 'block'; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /hls_js/simple/dai.css: -------------------------------------------------------------------------------- 1 | #video, 2 | #adUi { 3 | width: 640px; 4 | height: 360px; 5 | position: absolute; 6 | top: 35px; 7 | left: 0; 8 | } 9 | 10 | #adUi { 11 | cursor: pointer; 12 | } 13 | 14 | #play-button { 15 | position: absolute; 16 | top: 400px; 17 | left: 15px; 18 | } 19 | -------------------------------------------------------------------------------- /hls_js/simple/dai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

IMA SDK DAI Demo (HLS.JS)

12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /hls_js/simple/dai.js: -------------------------------------------------------------------------------- 1 | // [START init_player] 2 | // This stream will be played if ad-enabled playback fails. 3 | const BACKUP_STREAM = 4 | 'http://storage.googleapis.com/testtopbox-public/video_content/bbb/' + 5 | 'master.m3u8'; 6 | 7 | // Live stream asset key. 8 | // const TEST_ASSET_KEY = 'c-rArva4ShKVIAkNfy6HUQ'; 9 | 10 | // VOD content source and video IDs. 11 | const TEST_CONTENT_SOURCE_ID = '2548831'; 12 | const TEST_VIDEO_ID = 'tears-of-steel'; 13 | 14 | // Ad Manager network code. 15 | const NETWORK_CODE = '21775744923'; 16 | const API_KEY = null; 17 | 18 | // StreamManager which will be used to request ad-enabled streams. 19 | let streamManager; 20 | 21 | // hls.js video player 22 | const hls = new Hls(); 23 | 24 | // Video element 25 | let videoElement; 26 | 27 | // Ad UI element 28 | let adUiElement; 29 | 30 | // The play/resume button 31 | let playButton; 32 | 33 | // Whether the stream is currently in an ad break. 34 | let adBreak = false; 35 | 36 | /** 37 | * Initializes the video player. 38 | */ 39 | function initPlayer() { 40 | videoElement = document.getElementById('video'); 41 | playButton = document.getElementById('play-button'); 42 | adUiElement = document.getElementById('adUi'); 43 | createStreamManager(); 44 | listenForMetadata(); 45 | 46 | // Show the video controls when the video is paused during an ad break, 47 | // and hide them when ad playback resumes. 48 | videoElement.addEventListener('pause', () => { 49 | if (adBreak) { 50 | showVideoControls(); 51 | } 52 | }); 53 | videoElement.addEventListener('play', () => { 54 | if (adBreak) { 55 | hideVideoControls(); 56 | } 57 | }); 58 | 59 | playButton.addEventListener('click', () => { 60 | console.log('initiatePlayback'); 61 | requestStream(); 62 | // Hide this play button after the first click to request the stream. 63 | playButton.style.display = 'none'; 64 | }); 65 | } 66 | // [END init_player] 67 | 68 | // [START create_stream_manager] 69 | /** 70 | * Create the StreamManager and listen to stream events. 71 | */ 72 | function createStreamManager() { 73 | streamManager = 74 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 75 | streamManager.addEventListener( 76 | google.ima.dai.api.StreamEvent.Type.LOADED, onStreamEvent); 77 | streamManager.addEventListener( 78 | google.ima.dai.api.StreamEvent.Type.ERROR, onStreamEvent); 79 | streamManager.addEventListener( 80 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, onStreamEvent); 81 | streamManager.addEventListener( 82 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, onStreamEvent); 83 | } 84 | // [END create_stream_manager] 85 | 86 | // [START request_stream] 87 | /** 88 | * Makes a stream request and plays the stream. 89 | */ 90 | function requestStream() { 91 | requestVODStream(TEST_CONTENT_SOURCE_ID, TEST_VIDEO_ID, NETWORK_CODE, API_KEY); 92 | // Uncomment line below and comment one above to request a LIVE stream. 93 | // requestLiveStream(TEST_ASSET_KEY, NETWORK_CODE, API_KEY); 94 | } 95 | 96 | /** 97 | * Requests a Live stream with ads. 98 | * @param {string} assetKey 99 | * @param {?string} networkCode 100 | * @param {?string} apiKey 101 | */ 102 | function requestLiveStream(assetKey, networkCode, apiKey) { 103 | const streamRequest = new google.ima.dai.api.LiveStreamRequest(); 104 | streamRequest.assetKey = assetKey; 105 | streamRequest.networkCode = networkCode; 106 | streamRequest.apiKey = apiKey; 107 | streamManager.requestStream(streamRequest); 108 | } 109 | 110 | /** 111 | * Requests a VOD stream with ads. 112 | * @param {string} cmsId 113 | * @param {string} videoId 114 | * @param {?string} networkCode 115 | * @param {?string} apiKey 116 | */ 117 | function requestVODStream(cmsId, videoId, networkCode, apiKey) { 118 | const streamRequest = new google.ima.dai.api.VODStreamRequest(); 119 | streamRequest.contentSourceId = cmsId; 120 | streamRequest.videoId = videoId; 121 | streamRequest.networkCode = networkCode; 122 | streamRequest.apiKey = apiKey; 123 | streamManager.requestStream(streamRequest); 124 | } 125 | // [END request_stream] 126 | 127 | // [START listen_for_metadata] 128 | /** 129 | * Set up metadata listeners to pass metadata to the StreamManager. 130 | */ 131 | function listenForMetadata() { 132 | // Only used in LIVE streams. Timed metadata is handled differently 133 | // by different video players, and the IMA SDK provides two ways 134 | // to pass in metadata, StreamManager.processMetadata() and 135 | // StreamManager.onTimedMetadata(). 136 | // 137 | // Use StreamManager.onTimedMetadata() if your video player parses 138 | // the metadata itself. 139 | // Use StreamManager.processMetadata() if your video player provides raw 140 | // ID3 tags, as with hls.js. 141 | hls.on(Hls.Events.FRAG_PARSING_METADATA, function(event, data) { 142 | if (streamManager && data) { 143 | // For each ID3 tag in our metadata, we pass in the type - ID3, the 144 | // tag data (a byte array), and the presentation timestamp (PTS). 145 | data.samples.forEach(function(sample) { 146 | streamManager.processMetadata('ID3', sample.data, sample.pts); 147 | }); 148 | } 149 | }); 150 | } 151 | // [END listen_for_metadata] 152 | 153 | // [START stream_event] 154 | /** 155 | * Responds to a stream event. 156 | * @param {!google.ima.dai.api.StreamEvent} e 157 | */ 158 | function onStreamEvent(e) { 159 | switch (e.type) { 160 | case google.ima.dai.api.StreamEvent.Type.LOADED: 161 | console.log('Stream loaded'); 162 | loadUrl(e.getStreamData().url); 163 | break; 164 | case google.ima.dai.api.StreamEvent.Type.ERROR: 165 | console.log('Error loading stream, playing backup stream.' + e); 166 | loadUrl(BACKUP_STREAM); 167 | break; 168 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 169 | console.log('Ad Break Started'); 170 | adBreak = true; 171 | hideVideoControls(); 172 | break; 173 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 174 | console.log('Ad Break Ended'); 175 | adBreak = false; 176 | showVideoControls(); 177 | break; 178 | default: 179 | break; 180 | } 181 | } 182 | 183 | /** 184 | * Loads and plays a Url. 185 | * @param {string} url 186 | */ 187 | function loadUrl(url) { 188 | console.log('Loading:' + url); 189 | hls.loadSource(url); 190 | hls.attachMedia(videoElement); 191 | hls.on(Hls.Events.MANIFEST_PARSED, function() { 192 | console.log('Video Play'); 193 | videoElement.play(); 194 | }); 195 | } 196 | // [END stream_event] 197 | 198 | // [START video_controls] 199 | /** 200 | * Hides the video controls. 201 | */ 202 | function hideVideoControls() { 203 | videoElement.controls = false; 204 | adUiElement.style.display = 'block'; 205 | } 206 | 207 | /** 208 | * Shows the video controls. 209 | */ 210 | function showVideoControls() { 211 | videoElement.controls = true; 212 | adUiElement.style.display = 'none'; 213 | } 214 | // [END video_controls] 215 | -------------------------------------------------------------------------------- /native/simple/dai.css: -------------------------------------------------------------------------------- 1 | #video, #click { 2 | width: 640px; 3 | height: 360px; 4 | position: absolute; 5 | top: 35px; 6 | left: 0px; 7 | } 8 | 9 | #click { 10 | cursor: pointer; 11 | display: none; 12 | } 13 | 14 | #banner { 15 | width: 100%; 16 | height: 35px; 17 | background-color: black; 18 | color: white; 19 | position: absolute; 20 | top: 0px; 21 | left: 0px; 22 | } 23 | -------------------------------------------------------------------------------- /native/simple/dai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

IMA SDK DAI Demo (HLS.JS)

9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /native/simple/dai.js: -------------------------------------------------------------------------------- 1 | // This stream will be played if ad-enabled playback fails. 2 | const BACKUP_STREAM = 3 | 'http://storage.googleapis.com/testtopbox-public/video_content/bbb/' + 4 | 'master.m3u8'; 5 | 6 | // Live stream asset key. 7 | // const TEST_ASSET_KEY = 'c-rArva4ShKVIAkNfy6HUQ'; 8 | 9 | // VOD content source and video IDs. 10 | const TEST_CONTENT_SOURCE_ID = '2548831'; 11 | const TEST_VIDEO_ID = 'tears-of-steel'; 12 | 13 | const NETWORK_CODE = '21775744923'; 14 | const API_KEY = null; 15 | 16 | // StreamManager which will be used to request ad-enabled streams. 17 | let streamManager; 18 | 19 | // Video element 20 | let videoElement; 21 | 22 | // Ad UI element 23 | let adUiElement; 24 | 25 | // Whether the stream is currently in an ad break. 26 | let isAdBreak; 27 | 28 | /** 29 | * Initializes the video player. 30 | */ 31 | function initPlayer() { 32 | videoElement = document.getElementById('video'); 33 | adUiElement = document.getElementById('adUi'); 34 | 35 | videoElement.addEventListener('pause', onStreamPause); 36 | videoElement.addEventListener('play', onStreamPlay); 37 | 38 | streamManager = 39 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 40 | streamManager.addEventListener( 41 | [ 42 | google.ima.dai.api.StreamEvent.Type.LOADED, 43 | google.ima.dai.api.StreamEvent.Type.ERROR, 44 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 45 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED 46 | ], 47 | onStreamEvent, false); 48 | 49 | requestVODStream(TEST_CONTENT_SOURCE_ID, TEST_VIDEO_ID, NETWORK_CODE, API_KEY); 50 | // Uncomment line below and comment one above to request a LIVE stream. 51 | // requestLiveStream(TEST_ASSET_KEY, NETWORK_CODE, API_KEY); 52 | } 53 | 54 | /** 55 | * Requests a Live stream with ads. 56 | * @param {string} assetKey 57 | * @param {?string} networkCode 58 | * @param {?string} apiKey 59 | */ 60 | function requestLiveStream(assetKey, networkCode, apiKey) { 61 | const streamRequest = new google.ima.dai.api.LiveStreamRequest(); 62 | streamRequest.assetKey = assetKey; 63 | streamRequest.networkCode = networkCode; 64 | streamRequest.apiKey = apiKey; 65 | streamManager.requestStream(streamRequest); 66 | } 67 | 68 | /** 69 | * Requests a VOD stream with ads. 70 | * @param {string} cmsId 71 | * @param {string} videoId 72 | * @param {?string} networkCode 73 | * @param {?string} apiKey 74 | */ 75 | function requestVODStream(cmsId, videoId, networkCode, apiKey) { 76 | const streamRequest = new google.ima.dai.api.VODStreamRequest(); 77 | streamRequest.contentSourceId = cmsId; 78 | streamRequest.videoId = videoId; 79 | streamRequest.networkCode = networkCode; 80 | streamRequest.apiKey = apiKey; 81 | streamManager.requestStream(streamRequest); 82 | } 83 | 84 | /** 85 | * Responds to a stream event. 86 | * @param {!google.ima.dai.api.StreamEvent} e 87 | */ 88 | function onStreamEvent(e) { 89 | switch (e.type) { 90 | case google.ima.dai.api.StreamEvent.Type.LOADED: 91 | console.log('Stream loaded'); 92 | loadUrl(e.getStreamData().url); 93 | break; 94 | case google.ima.dai.api.StreamEvent.Type.ERROR: 95 | console.log('Error loading stream, playing backup stream.' + e); 96 | loadUrl(BACKUP_STREAM); 97 | break; 98 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 99 | console.log('Ad Break Started'); 100 | isAdBreak = true; 101 | videoElement.controls = false; 102 | adUiElement.style.display = 'block'; 103 | break; 104 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 105 | console.log('Ad Break Ended'); 106 | isAdBreak = false; 107 | videoElement.controls = true; 108 | adUiElement.style.display = 'none'; 109 | break; 110 | default: 111 | break; 112 | } 113 | } 114 | 115 | /** 116 | * Loads and plays a Url. 117 | * @param {string} url 118 | */ 119 | function loadUrl(url) { 120 | console.log('Loading:' + url); 121 | videoElement.src = url; 122 | videoElement.textTracks.addEventListener('addtrack', onAddTrack); 123 | videoElement.controls = true; 124 | } 125 | 126 | /** 127 | * Called to process metadata for the video element. 128 | * @param {!Event} event The add track event. 129 | */ 130 | function onAddTrack(event) { 131 | const track = event.track; 132 | if (track.kind === 'metadata') { 133 | track.mode = 'hidden'; 134 | track.addEventListener('cuechange', (unusedEvent) => { 135 | for (const cue of track.activeCues) { 136 | const metadata = {}; 137 | metadata[cue.value.key] = cue.value.data; 138 | streamManager.onTimedMetadata(metadata); 139 | } 140 | }); 141 | } 142 | } 143 | 144 | /** 145 | * Shows the video controls so users can resume after stream is paused. 146 | */ 147 | function onStreamPause() { 148 | console.log('paused'); 149 | if (isAdBreak) { 150 | videoElement.controls = true; 151 | adUiDiv.style.display = 'none'; 152 | } 153 | } 154 | 155 | /** 156 | * Hides the video controls if resumed during an ad break. 157 | */ 158 | function onStreamPlay() { 159 | console.log('played'); 160 | if (isAdBreak) { 161 | videoElement.controls = false; 162 | adUiDiv.style.display = 'block'; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /podserving/dash_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DAI Podserving Client Sample (DASH streams) 7 | 8 | 9 |
10 |

Pod Serving Sample (DASH streams)

11 | 16 | 20 |
21 | 25 | 29 |
30 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
Not Playing
46 |
47 |
48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /podserving/dash_js/player.js: -------------------------------------------------------------------------------- 1 | let adPlaying = false; 2 | let dashPlayer; 3 | let streamManager; 4 | let isVodStream; 5 | let streamId; 6 | 7 | const streamUrl = document.getElementById('stream-manifest'); 8 | const networkCode = document.getElementById('network-code'); 9 | const assetkey = document.getElementById('custom-asset-key'); 10 | const apikey = document.getElementById('api-key'); 11 | const liveStreamButton = document.getElementById('live-stream-request'); 12 | const vodStreamButton = document.getElementById('vod-stream-request'); 13 | const requestButton = document.getElementById('request-pod'); 14 | 15 | const adUiElement = document.getElementById('video-ad-ui'); 16 | const videoElement = document.getElementById('video'); 17 | 18 | /** 19 | * begin processing JavaScript 20 | */ 21 | function init() { 22 | logText('Initializing'); 23 | 24 | // Clear the stream parameters when switching stream types. 25 | liveStreamButton.addEventListener('click', resetStreamParameters); 26 | vodStreamButton.addEventListener('click', resetStreamParameters); 27 | 28 | requestButton.onclick = (e) => { 29 | e.preventDefault(); 30 | if (liveStreamButton.checked) { 31 | if (!networkCode.value || !assetkey.value || !streamUrl.value) { 32 | logText('ERROR: Network Code, Asset Key, and Stream URL are required ' + 33 | 'for livestream requests.'); 34 | setStatus('Error'); 35 | return; 36 | } 37 | } else { 38 | if (!networkCode.value || !streamUrl.value) { 39 | logText('ERROR: Network Code and Stream URL are required for VOD' + 40 | 'streams.'); 41 | setStatus('Error'); 42 | return; 43 | } 44 | } 45 | 46 | initiateStreamManager(); 47 | // clear DASH.js instance, if in use. 48 | dashPlayer?.destroy(); 49 | 50 | if (liveStreamButton.checked) { 51 | logText('Requesting PodServing Live Stream'); 52 | requestPodLiveStream(networkCode.value, assetkey.value, apikey.value); 53 | isVodStream = false; 54 | } else { 55 | logText('Requesting PodServing VOD Stream'); 56 | requestPodVodStream(networkCode.value); 57 | isVodStream = true; 58 | } 59 | }; 60 | } 61 | 62 | /** 63 | * Clears the stream parameter input fields and updates UI to show only the 64 | * relevant inputs for the selected stream type. 65 | */ 66 | function resetStreamParameters() { 67 | streamUrl.value = ''; 68 | networkCode.value = ''; 69 | assetkey.value = ''; 70 | apikey.value = ''; 71 | 72 | const liveStreamParamContainer = 73 | document.getElementById('live-stream-only-params'); 74 | if (liveStreamButton.checked) { 75 | liveStreamParamContainer.classList.remove('hidden'); 76 | } else { 77 | liveStreamParamContainer.classList.add('hidden'); 78 | } 79 | } 80 | 81 | /** 82 | * Creates the IMA StreamManager and sets ad event listeners. 83 | */ 84 | function initiateStreamManager() { 85 | // generate a stream manager, on first request 86 | if (!streamManager) { 87 | streamManager = 88 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 89 | // Add event listeners 90 | streamManager.addEventListener( 91 | [ 92 | google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED, 93 | google.ima.dai.api.StreamEvent.Type.ERROR, 94 | google.ima.dai.api.StreamEvent.Type.CLICK, 95 | google.ima.dai.api.StreamEvent.Type.STARTED, 96 | google.ima.dai.api.StreamEvent.Type.FIRST_QUARTILE, 97 | google.ima.dai.api.StreamEvent.Type.MIDPOINT, 98 | google.ima.dai.api.StreamEvent.Type.THIRD_QUARTILE, 99 | google.ima.dai.api.StreamEvent.Type.COMPLETE, 100 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 101 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, 102 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, 103 | google.ima.dai.api.StreamEvent.Type.PAUSED, 104 | google.ima.dai.api.StreamEvent.Type.RESUMED 105 | ], 106 | onStreamEvent, false); 107 | } 108 | } 109 | 110 | /** 111 | * Request a pod livestream from Google. 112 | * @param {string} networkCode - the network code. 113 | * @param {string} customAssetKey - the asset key. 114 | * @param {string} apiKey - the api key (optional). 115 | */ 116 | function requestPodLiveStream(networkCode, customAssetKey, apiKey) { 117 | // Generate a PodServing Stream Request 118 | const streamRequest = new google.ima.dai.api.PodStreamRequest(); 119 | streamRequest.networkCode = networkCode; 120 | streamRequest.customAssetKey = customAssetKey; 121 | streamRequest.apiKey = apiKey; 122 | streamRequest.format = 'dash'; // Defaults to 'hls' if not set. 123 | streamManager.requestStream(streamRequest); 124 | } 125 | 126 | /** 127 | * Request a pod VOD stream from Google. 128 | * @param {string} networkCode - the network code. 129 | */ 130 | function requestPodVodStream(networkCode) { 131 | const streamRequest = new google.ima.dai.api.PodVodStreamRequest(); 132 | streamRequest.networkCode = networkCode; 133 | streamRequest.format = 'dash'; // Defaults to 'hls' if not set. 134 | streamManager.requestStream(streamRequest); 135 | } 136 | 137 | /** 138 | * Handle stream events 139 | * @param {!Event} e - the event object 140 | */ 141 | function onStreamEvent(e) { 142 | switch (e.type) { 143 | // Once PodServing stream is initialized, build request 144 | // for the video stream, including the podserving stream id 145 | case google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED: 146 | streamId = e.getStreamData().streamId; 147 | logText('Stream initialized: ' + streamId); 148 | if (isVodStream) { 149 | // For VOD streams, IMA requires a call to 150 | // StreamManager.loadStreamMetadata() in response to getting the 151 | // stream request URL from the video technology partner (VTP) you are 152 | // using. It will be similar to this code snippet, but may vary 153 | // depending on your VTP. 154 | // 155 | // vtpInterface.requestStreamURL({ 156 | // 'streamId': streamId, 157 | // }) 158 | // .then( () => { 159 | // streamManager.loadStreamMetadata(); 160 | // }, (error) => { 161 | // // Handle the error. 162 | // }); 163 | console.error('VOD stream error: You will need to edit the code to ' + 164 | 'make a call to streamManager.loadStreamMetadata() once ' + 165 | 'you get the stream URL from your VTP.'); 166 | } else { 167 | const url = buildStreamURL(streamId); 168 | loadStream(url); 169 | } 170 | break; 171 | case google.ima.dai.api.StreamEvent.Type.LOADED: 172 | if (isVodStream) { 173 | const url = buildStreamURL(streamId); 174 | loadStream(url); 175 | } 176 | break; 177 | // Log Errors 178 | case google.ima.dai.api.StreamEvent.Type.ERROR: 179 | setStatus('Error'); 180 | logText('ERROR: ' + e.getStreamData().errorMessage); 181 | break; 182 | // Hide video controls while ad is playing 183 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 184 | logText('Ad Break Started'); 185 | adPlaying = true; 186 | hideControls(); 187 | setStatus('Playing ads'); 188 | break; 189 | // show video controls when ad ends 190 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 191 | logText('Ad Break Ended'); 192 | adPlaying = false; 193 | showControls(); 194 | setStatus('Playing content'); 195 | break; 196 | // update ad countdown timers 197 | case google.ima.dai.api.StreamEvent.Type.AD_PROGRESS: 198 | updateAdCountdown(e.getStreamData().adProgressData); 199 | break; 200 | default: 201 | logEvent(e); 202 | break; 203 | } 204 | } 205 | 206 | /** 207 | * Inserts streamId into stream URL 208 | * @param {string} streamId - The Stream ID, from the pod stream request 209 | * @return {string} The stream url with StreamId populated 210 | */ 211 | function buildStreamURL(streamId) { 212 | let url = streamUrl.value; 213 | return url.replace('[[STREAMID]]', streamId); 214 | } 215 | 216 | /** 217 | * Updates the stream info with ad Countdowns 218 | * @param {Object!} data - The Ad Event data 219 | */ 220 | function updateAdCountdown(data) { 221 | const adPosition = data.adPosition; 222 | const totalAds = data.totalAds; 223 | const adBreakDuration = data.adBreakDuration; 224 | const duration = data.duration; 225 | const currentTime = data.currentTime; 226 | let status = 'Ad: ' + adPosition + ' of ' + totalAds + ' | ' + 227 | toHHMMSS(currentTime) + '/' + toHHMMSS(duration) + ' | ' + 228 | 'Break Duration: ' + adBreakDuration; 229 | setStatus(status); 230 | } 231 | 232 | /** 233 | * shows the video element controls and hides ad overlay 234 | */ 235 | function showControls() { 236 | adUiElement.style.display = 'none'; 237 | if (!videoElement.hasAttribute('controls')) { 238 | videoElement.setAttribute('controls', 'controls'); 239 | } 240 | } 241 | 242 | /** 243 | * hides the video element controls and shows ad overlay 244 | */ 245 | function hideControls() { 246 | adUiElement.style.display = 'initial'; 247 | if (videoElement.hasAttribute('controls')) { 248 | videoElement.removeAttribute('controls'); 249 | } 250 | } 251 | 252 | /** 253 | * Loads the video stream and attaches event listeners to update podserving 254 | * stream 255 | * @param {string} url - The stream URL 256 | */ 257 | function loadStream(url) { 258 | logText('Load stream: ' + url); 259 | 260 | if (dashPlayer) { 261 | dashPlayer.destroy(); 262 | } 263 | dashPlayer = dashjs.MediaPlayer().create(); 264 | dashPlayer.initialize(videoElement); 265 | dashPlayer.attachSource(url); 266 | // Listen for metadata events to pass to the streamManager. 267 | // The 'dashString' variable used here is specific to Google pod serving, and 268 | // may be different in various implementations. 269 | const dashString = 'https://developer.apple.com/streaming/emsg-id3'; 270 | dashPlayer.on(dashString, processMetadata); 271 | dashPlayer.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, loadlistener); 272 | dashPlayer.setMute(true); 273 | 274 | videoElement.onplay = onPlay; 275 | videoElement.onpause = onPause; 276 | videoElement.onplaying = onPlaying; 277 | } 278 | 279 | /** 280 | * Listen dor the first manifest to load and set playback status. 281 | */ 282 | function loadlistener() { 283 | logText('load listener returns'); 284 | setStatus('Ready to Play'); 285 | showControls(); 286 | 287 | // This listener must be removed, otherwise it triggers as addional 288 | // manifests are loaded. The manifest is loaded once for the content, 289 | // but additional manifests are loaded for upcoming ad breaks. 290 | dashPlayer.off(dashjs.MediaPlayer.events.MANIFEST_LOADED, loadlistener); 291 | } 292 | 293 | /** 294 | * Process metadata from DASH.js 295 | * @param {!Event} metadataEvent - the metadata event object 296 | */ 297 | function processMetadata(metadataEvent) { 298 | const messageData = metadataEvent.event.messageData; 299 | const timestamp = metadataEvent.event.calculatedPresentationTime; 300 | 301 | // Use StreamManager.processMetadata() if your video player provides raw 302 | // ID3 tags, as with dash.js. Otherwise, use StreamManager.onTimedMetadata() 303 | // for handling processed metadata. 304 | streamManager.processMetadata('ID3', messageData, timestamp); 305 | } 306 | 307 | /** 308 | * Set video controls when play starts 309 | * @param {!Object} event - The event object (unused) 310 | */ 311 | function onPlay(event) { 312 | if (adPlaying) { 313 | setStatus('Playing ads'); 314 | hideControls(); 315 | } else { 316 | setStatus('Playing content'); 317 | showControls(); 318 | } 319 | } 320 | 321 | /** 322 | * Failsafe to show video controls when paused 323 | * @param {!Object} event - The event object (unused) 324 | */ 325 | function onPause(event) { 326 | setStatus('Paused'); 327 | showControls(); 328 | } 329 | 330 | /** 331 | * Failsafe to show/hide video controls 332 | * @param {!Object} event - The event object (unused) 333 | */ 334 | function onPlaying(event) { 335 | if (adPlaying) { 336 | hideControls(); 337 | } else { 338 | showControls(); 339 | } 340 | } 341 | 342 | /** 343 | * helper to update contents of #countdown div 344 | * @param {string} status - The message to output 345 | */ 346 | function setStatus(status) { 347 | document.getElementById('countdown').textContent = status; 348 | } 349 | 350 | /** 351 | * Simple wrapper function for logging 352 | * @param {string} text - The message to log 353 | */ 354 | function logText(text) { 355 | console.log(text); 356 | } 357 | 358 | /** 359 | * Format ad event for logging 360 | * @param {!Event} event - The ad event object 361 | */ 362 | function logEvent(event) { 363 | const ad = event.getAd(); 364 | const adPodInfo = ad ? ad.getAdPodInfo() : null; 365 | const type = event.type; 366 | const title = ad ? ad.getTitle() : ''; 367 | const position = adPodInfo ? adPodInfo.getAdPosition() : 0; 368 | const totalAds = adPodInfo ? adPodInfo.getTotalAds() : 0; 369 | logText('Stream manager event:' + type); 370 | logText(`Ad ${position}/${totalAds}: ${title}` + type); 371 | } 372 | 373 | /** 374 | * Utility function to convert seconds to human-readible time string 375 | * @param {!float} secNum - number of seconds 376 | * @return {string} the same duration, represented in h:i:s format 377 | */ 378 | function toHHMMSS(secNum) { 379 | let hours = Math.floor(secNum / 3600); 380 | const minutes = Math.floor((secNum - (hours * 3600)) / 60); 381 | let seconds = Math.floor(secNum - (hours * 3600) - (minutes * 60)); 382 | 383 | let hourStr = ''; 384 | if (hours > 0) { 385 | if (hours < 10) { 386 | hours = '0' + hours; 387 | } 388 | hourStr += hours + ':'; 389 | } 390 | if (seconds < 10) { 391 | seconds = '0' + seconds; 392 | } 393 | return hourStr + minutes + ':' + seconds; 394 | } 395 | 396 | init(); 397 | -------------------------------------------------------------------------------- /podserving/dash_js/styles.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: white; 3 | font-family: sans-serif; 4 | font-size: 18px; 5 | } 6 | #content { 7 | position: relative; 8 | width: 100%; 9 | max-width: 640px; 10 | } 11 | label { 12 | display: block; 13 | margin-bottom: 10px; 14 | } 15 | label span { 16 | display: block; 17 | font-weight: bold; 18 | } 19 | 20 | label input, button { 21 | display: block; 22 | width: 100%; 23 | padding: 5px; 24 | font-size: 18px; 25 | } 26 | 27 | label span.note { 28 | font-weight: normal; 29 | font-style: italic; 30 | width: auto; 31 | font-size: 15px; 32 | } 33 | 34 | button { 35 | cursor: pointer; 36 | } 37 | 38 | #request-type { 39 | display: block; 40 | } 41 | 42 | #request-type label { 43 | display: inline-block; 44 | } 45 | 46 | #request-type input { 47 | display: inline-block; 48 | width: auto; 49 | margin-right: 30px; 50 | } 51 | 52 | #video-container { 53 | position: relative; 54 | padding-bottom: 56.25%; 55 | background-color: black; 56 | } 57 | #video-ad-ui { 58 | position: absolute; 59 | width: 100%; 60 | height: 100%; 61 | z-index: 1; 62 | } 63 | #video { 64 | position: absolute; 65 | width:100%; 66 | height:100%; 67 | } 68 | 69 | #countdown { 70 | margin: 5px 0; 71 | } 72 | 73 | .hidden { 74 | display: none; 75 | } 76 | -------------------------------------------------------------------------------- /podserving/hls_js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DAI Podserving Client Sample (HLS streams) 7 | 8 | 9 |
10 |

Pod Serving Sample (Unknown)

11 | 16 | 20 |
21 | 25 | 29 |
30 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
Not Playing
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /podserving/hls_js/player.js: -------------------------------------------------------------------------------- 1 | let adPlaying = false; 2 | let hls; 3 | let streamManager; 4 | let isVodStream; 5 | let streamId; 6 | 7 | const pmElement = document.getElementById('playbackMethod'); 8 | 9 | const streamUrl = document.getElementById('stream-manifest'); 10 | const networkCode = document.getElementById('network-code'); 11 | const assetkey = document.getElementById('custom-asset-key'); 12 | const apikey = document.getElementById('api-key'); 13 | const liveStreamButton = document.getElementById('live-stream-request'); 14 | const vodStreamButton = document.getElementById('vod-stream-request'); 15 | const requestButton = document.getElementById('request-pod'); 16 | 17 | const adUiElement = document.getElementById('video-ad-ui'); 18 | const videoElement = document.getElementById('video'); 19 | 20 | /** 21 | * begin processing JavaScript 22 | */ 23 | function init() { 24 | logText('Initializing'); 25 | if (useNativePlayer()) { 26 | pmElement.textContent = 'Native Player'; 27 | } else { 28 | pmElement.textContent = 'HLS.js'; 29 | } 30 | 31 | // Clear the stream parameters when switching stream types. 32 | liveStreamButton.addEventListener('click', resetStreamParameters); 33 | vodStreamButton.addEventListener('click', resetStreamParameters); 34 | 35 | requestButton.onclick = (e) => { 36 | e.preventDefault(); 37 | if (liveStreamButton.checked) { 38 | if (!networkCode.value || !assetkey.value || !streamUrl.value) { 39 | logText('ERROR: Network Code, Asset Key, and Stream URL are required' + 40 | ' for live streams.'); 41 | setStatus('Error'); 42 | return; 43 | } 44 | isVodStream = false; 45 | } else { 46 | if (!networkCode.value || !streamUrl.value) { 47 | logText('ERROR: Network Code and Stream URL are required for VOD' + 48 | 'streams.'); 49 | setStatus('Error'); 50 | return; 51 | } 52 | isVodStream = true; 53 | } 54 | 55 | initiateStreamManager(); 56 | // clear HLS.js instance, if in use. 57 | hls?.destroy(); 58 | 59 | if (!isVodStream) { 60 | logText('Requesting PodServing Live Stream'); 61 | requestPodLiveStream(networkCode.value, assetkey.value, apikey.value); 62 | } else { 63 | logText('Requesting PodServing VOD Stream'); 64 | requestPodVodStream(networkCode.value); 65 | } 66 | }; 67 | } 68 | 69 | /** 70 | * Clears the stream parameter input fields and updates UI to show only the 71 | * relevant inputs for the selected stream type. 72 | */ 73 | function resetStreamParameters() { 74 | streamUrl.value = ''; 75 | networkCode.value = ''; 76 | assetkey.value = ''; 77 | apikey.value = ''; 78 | 79 | const liveStreamParamContainer = 80 | document.getElementById('live-stream-only-params'); 81 | if (liveStreamButton.checked) { 82 | liveStreamParamContainer.classList.remove('hidden'); 83 | } else { 84 | liveStreamParamContainer.classList.add('hidden'); 85 | } 86 | } 87 | 88 | /** 89 | * Check browser for HLS support 90 | * @return {boolean} is the browser safari. 91 | */ 92 | function useNativePlayer() { 93 | // this could be a more advanced check, but instead is a trivial navigator 94 | return navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && 95 | navigator.userAgent && navigator.userAgent.indexOf('CriOS') == -1 && 96 | navigator.userAgent.indexOf('FxiOS') == -1; 97 | } 98 | 99 | /** 100 | * Creates the IMA StreamManager and sets ad event listeners. 101 | */ 102 | function initiateStreamManager() { 103 | // generate a stream manager, on first request 104 | if (!streamManager) { 105 | streamManager = 106 | new google.ima.dai.api.StreamManager(videoElement, adUiElement); 107 | // Add event listeners 108 | streamManager.addEventListener( 109 | [ 110 | google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED, 111 | google.ima.dai.api.StreamEvent.Type.ERROR, 112 | google.ima.dai.api.StreamEvent.Type.CLICK, 113 | google.ima.dai.api.StreamEvent.Type.STARTED, 114 | google.ima.dai.api.StreamEvent.Type.FIRST_QUARTILE, 115 | google.ima.dai.api.StreamEvent.Type.MIDPOINT, 116 | google.ima.dai.api.StreamEvent.Type.THIRD_QUARTILE, 117 | google.ima.dai.api.StreamEvent.Type.COMPLETE, 118 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED, 119 | google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED, 120 | google.ima.dai.api.StreamEvent.Type.AD_PROGRESS, 121 | google.ima.dai.api.StreamEvent.Type.PAUSED, 122 | google.ima.dai.api.StreamEvent.Type.RESUMED 123 | ], 124 | onStreamEvent, false); 125 | } 126 | } 127 | 128 | /** 129 | * Request a pod livestream from Google. 130 | * @param {string} networkCode - the network code. 131 | * @param {string} customAssetKey - the asset key. 132 | * @param {string} apiKey - the api key (optional). 133 | */ 134 | function requestPodLiveStream(networkCode, customAssetKey, apiKey) { 135 | const streamRequest = new google.ima.dai.api.PodStreamRequest(); 136 | streamRequest.networkCode = networkCode; 137 | streamRequest.customAssetKey = customAssetKey; 138 | streamRequest.apiKey = apiKey; 139 | streamManager.requestStream(streamRequest); 140 | } 141 | 142 | /** 143 | * Request a pod VOD stream from Google. 144 | * @param {string} networkCode - the network code. 145 | */ 146 | function requestPodVodStream(networkCode) { 147 | const streamRequest = new google.ima.dai.api.PodVodStreamRequest(); 148 | streamRequest.networkCode = networkCode; 149 | streamManager.requestStream(streamRequest); 150 | } 151 | 152 | /** 153 | * Handle stream events 154 | * @param {!Event} e - the event object 155 | */ 156 | function onStreamEvent(e) { 157 | switch (e.type) { 158 | // Once PodServing stream is initialized, build request 159 | // for the video stream, including the podserving stream id 160 | case google.ima.dai.api.StreamEvent.Type.STREAM_INITIALIZED: 161 | streamId = e.getStreamData().streamId; 162 | logText('Stream initialized: ' + streamId); 163 | if (isVodStream) { 164 | // For VOD streams, IMA requires a call to 165 | // StreamManager.loadStreamMetadata() in response to getting the 166 | // stream request URL from the video technology partner (VTP) you are 167 | // using. It will be similar to this code snippet, but may vary 168 | // depending on your VTP. 169 | // 170 | // vtpInterface.requestStreamURL({ 171 | // 'streamId': streamId, 172 | // }) 173 | // .then( () => { 174 | // streamManager.loadStreamMetadata(); 175 | // }, (error) => { 176 | // // Handle the error. 177 | // }); 178 | console.error('VOD stream error: You will need to edit the code to ' + 179 | 'make a call to streamManager.loadStreamMetadata() once ' + 180 | 'you get the stream URL from your VTP.'); 181 | } else { 182 | const url = buildStreamURL(streamId); 183 | loadStream(url); 184 | } 185 | break; 186 | case google.ima.dai.api.StreamEvent.Type.LOADED: 187 | if (isVodStream) { 188 | const url = buildStreamURL(streamId); 189 | loadStream(url); 190 | } 191 | break; 192 | // Log Errors 193 | case google.ima.dai.api.StreamEvent.Type.ERROR: 194 | setStatus('Error'); 195 | logText('ERROR: ' + e.getStreamData().errorMessage); 196 | break; 197 | // Hide video controls while ad is playing 198 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_STARTED: 199 | logText('Ad Break Started'); 200 | adPlaying = true; 201 | hideControls(); 202 | setStatus('Playing ads'); 203 | break; 204 | // show video controls when ad ends 205 | case google.ima.dai.api.StreamEvent.Type.AD_BREAK_ENDED: 206 | logText('Ad Break Ended'); 207 | adPlaying = false; 208 | showControls(); 209 | setStatus('Playing content'); 210 | break; 211 | // update ad countdown timers 212 | case google.ima.dai.api.StreamEvent.Type.AD_PROGRESS: 213 | updateAdCountdown(e.getStreamData().adProgressData); 214 | break; 215 | default: 216 | logEvent(e); 217 | break; 218 | } 219 | } 220 | 221 | /** 222 | * Inserts streamId into stream URL 223 | * @param {string} streamId - The Stream ID, from the pod stream request 224 | * @return {string} The stream url with StreamId populated 225 | */ 226 | function buildStreamURL(streamId) { 227 | let url = streamUrl.value; 228 | return url.replace('[[STREAMID]]', streamId); 229 | } 230 | 231 | /** 232 | * Updates the stream info with ad Countdowns 233 | * @param {Object!} data - The Ad Event data 234 | */ 235 | function updateAdCountdown(data) { 236 | const adPosition = data.adPosition; 237 | const totalAds = data.totalAds; 238 | const adBreakDuration = data.adBreakDuration; 239 | const duration = data.duration; 240 | const currentTime = data.currentTime; 241 | let status = 'Ad: ' + adPosition + ' of ' + totalAds + ' | ' + 242 | toHHMMSS(currentTime) + '/' + toHHMMSS(duration) + ' | ' + 243 | 'Break Duration: ' + adBreakDuration; 244 | setStatus(status); 245 | } 246 | 247 | /** 248 | * shows the video element controls and hides ad overlay 249 | */ 250 | function showControls() { 251 | adUiElement.style.display = 'none'; 252 | if (!videoElement.hasAttribute('controls')) { 253 | videoElement.setAttribute('controls', 'controls'); 254 | } 255 | } 256 | 257 | /** 258 | * hides the video element controls and shows ad overlay 259 | */ 260 | function hideControls() { 261 | adUiElement.style.display = 'initial'; 262 | if (videoElement.hasAttribute('controls')) { 263 | videoElement.removeAttribute('controls'); 264 | } 265 | } 266 | 267 | /** 268 | * Loads the video stream and attaches event listeners to update podserving 269 | * stream 270 | * @param {string} url - The stream URL 271 | */ 272 | function loadStream(url) { 273 | logText('Load stream: ' + url); 274 | 275 | if (useNativePlayer()) { 276 | // safari can load HLS files natively 277 | videoElement.src = url; 278 | // listen for metadata events to pass to the streammanager 279 | videoElement.textTracks.addEventListener('addtrack', onAddTrack); 280 | onReadyToPlay(); 281 | } else { 282 | // non-safari browsers need HLS.js 283 | if (hls) { 284 | hls.destroy(); 285 | } 286 | hls = new Hls(); 287 | hls.loadSource(url); 288 | hls.attachMedia(videoElement); 289 | // listen for metadata events to pass to the streammanager 290 | hls.on(Hls.Events.FRAG_PARSING_METADATA, onTimedMetadata); 291 | hls.on(Hls.Events.MANIFEST_PARSED, onReadyToPlay); 292 | } 293 | videoElement.onplay = onPlay; 294 | videoElement.onpause = onPause; 295 | videoElement.onplaying = onPlaying; 296 | } 297 | 298 | /** 299 | * Set Playback Status 300 | */ 301 | function onReadyToPlay() { 302 | setStatus('Ready to Play'); 303 | showControls(); 304 | } 305 | 306 | /** 307 | * Parse timed metadata from HLS.js 308 | * @param {!Event} event - the event object 309 | * @param {!Array} data - the enumerable array of metadata objects 310 | */ 311 | function onTimedMetadata(event, data) { 312 | if (streamManager && data) { 313 | data.samples.forEach((sample) => { 314 | streamManager.processMetadata('ID3', sample.data, sample.pts); 315 | }); 316 | } 317 | } 318 | 319 | /** 320 | * Parse metadata from native player 321 | * @param {!Event} event - the addtrack event object 322 | */ 323 | function onAddTrack(event) { 324 | const track = event.track; 325 | if (streamManager && track.kind === 'metadata') { 326 | track.mode = 'hidden'; 327 | track.addEventListener('cuechange', (e) => { 328 | for (const cue of track.activeCues) { 329 | const metadata = {}; 330 | metadata[cue.value.key] = cue.value.data; 331 | streamManager.onTimedMetadata(metadata); 332 | } 333 | }); 334 | } 335 | } 336 | 337 | /** 338 | * Set video controls when play starts 339 | * @param {!Object} event - The event object (unused) 340 | */ 341 | function onPlay(event) { 342 | if (adPlaying) { 343 | setStatus('Playing ads'); 344 | hideControls(); 345 | } else { 346 | setStatus('Playing content'); 347 | showControls(); 348 | } 349 | } 350 | 351 | /** 352 | * Failsafe to show video controls when paused 353 | * @param {!Object} event - The event object (unused) 354 | */ 355 | function onPause(event) { 356 | setStatus('Paused'); 357 | showControls(); 358 | } 359 | 360 | /** 361 | * Failsafe to show/hide video controls 362 | * @param {!Object} event - The event object (unused) 363 | */ 364 | function onPlaying(event) { 365 | if (adPlaying) { 366 | hideControls(); 367 | } else { 368 | showControls(); 369 | } 370 | } 371 | 372 | /** 373 | * helper to update contents of #countdown div 374 | * @param {string} status - The message to output 375 | */ 376 | function setStatus(status) { 377 | document.getElementById('countdown').textContent = status; 378 | } 379 | 380 | /** 381 | * Simple wrapper function for logging 382 | * @param {string} text - The message to log 383 | */ 384 | function logText(text) { 385 | console.log(text); 386 | } 387 | 388 | /** 389 | * Format ad event for logging 390 | * @param {!Event} event - The ad event object 391 | */ 392 | function logEvent(event) { 393 | const ad = event.getAd(); 394 | const adPodInfo = ad ? ad.getAdPodInfo() : null; 395 | const type = event.type; 396 | const title = ad ? ad.getTitle() : ''; 397 | const position = adPodInfo ? adPodInfo.getAdPosition() : 0; 398 | const totalAds = adPodInfo ? adPodInfo.getTotalAds() : 0; 399 | logText('Stream manager event:' + type); 400 | logText(`Ad ${position}/${totalAds}: ${title}` + type); 401 | } 402 | 403 | /** 404 | * Utility function to convert seconds to human-readible time string 405 | * @param {!float} secNum - number of seconds 406 | * @return {string} the same duration, represented in h:i:s format 407 | */ 408 | function toHHMMSS(secNum) { 409 | let hours = Math.floor(secNum / 3600); 410 | const minutes = Math.floor((secNum - (hours * 3600)) / 60); 411 | let seconds = Math.floor(secNum - (hours * 3600) - (minutes * 60)); 412 | 413 | let hourStr = ''; 414 | if (hours > 0) { 415 | if (hours < 10) { 416 | hours = '0' + hours; 417 | } 418 | hourStr += hours + ':'; 419 | } 420 | if (seconds < 10) { 421 | seconds = '0' + seconds; 422 | } 423 | return hourStr + minutes + ':' + seconds; 424 | } 425 | 426 | init(); 427 | -------------------------------------------------------------------------------- /podserving/hls_js/styles.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: white; 3 | font-family: sans-serif; 4 | font-size: 18px; 5 | } 6 | #content { 7 | position: relative; 8 | width: 100%; 9 | max-width: 640px; 10 | } 11 | label { 12 | display: block; 13 | margin-bottom: 10px; 14 | } 15 | label span { 16 | display: block; 17 | font-weight: bold; 18 | } 19 | 20 | label input, button { 21 | display: block; 22 | width: 100%; 23 | padding: 5px; 24 | font-size: 18px; 25 | } 26 | 27 | label span.note { 28 | font-weight: normal; 29 | font-style: italic; 30 | width: auto; 31 | font-size: 15px; 32 | } 33 | 34 | button { 35 | cursor: pointer; 36 | } 37 | 38 | #request-type { 39 | display: block; 40 | } 41 | 42 | #request-type label { 43 | display: inline-block; 44 | } 45 | 46 | #request-type input { 47 | display: inline-block; 48 | width: auto; 49 | margin-right: 30px; 50 | } 51 | 52 | #video-container { 53 | position: relative; 54 | padding-bottom: 56.25%; 55 | background-color: black; 56 | } 57 | #video-ad-ui { 58 | position: absolute; 59 | width: 100%; 60 | height: 100%; 61 | z-index: 1; 62 | } 63 | #video { 64 | position: absolute; 65 | width:100%; 66 | height:100%; 67 | } 68 | 69 | #countdown { 70 | margin: 5px 0; 71 | } 72 | 73 | .hidden { 74 | display: none; 75 | } 76 | --------------------------------------------------------------------------------