├── .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 |
26 | Stream type:
27 |
28 |
29 |
30 | Live
31 |
32 |
33 |
34 | Video on demand (VOD)
35 |
36 |
37 |
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 |
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 |
12 | Stream URL:
13 |
14 | include [[STREAMID]] where the stream id should be inserted into your stream url
15 |
16 |
17 | Network Code:
18 |
19 |
20 |
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 |
12 | Stream URL:
13 |
14 | include [[STREAMID]] where the stream id should be inserted into your stream url
15 |
16 |
17 | Network Code:
18 |
19 |
20 |