@tivio/sdk-react 9.8.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -45
- package/README.md +5 -839
- package/README.md.bak +5 -839
- package/dist/index.d.ts +1511 -582
- package/dist/index.js +1 -1
- package/dist/sdk-react.d.ts +1665 -636
- package/package.json +11 -3
package/README.md.bak
CHANGED
|
@@ -8,9 +8,9 @@ settings in the administration of Tivio Studio while having the freedom to build
|
|
|
8
8
|
|
|
9
9
|
Install @tivio/sdk-react along with its peer dependencies
|
|
10
10
|
```sh
|
|
11
|
-
npm i react@
|
|
11
|
+
npm i react@18 react-dom@18 @tivio/sdk-react
|
|
12
12
|
# or
|
|
13
|
-
yarn add react@
|
|
13
|
+
yarn add react@18 react-dom@18 @tivio/sdk-react
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Initialization
|
|
@@ -63,337 +63,6 @@ const config: Config = {
|
|
|
63
63
|
};
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
### Create user with email and password
|
|
67
|
-
|
|
68
|
-
Returns user's firebase uid or null if error occurs
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
tivio.createUserWithEmailAndPassword(email: string, password: string, username: string, phoneNumber?: string | undefined): Promise<string | null>
|
|
72
|
-
```
|
|
73
|
-
**Example**
|
|
74
|
-
```typescript
|
|
75
|
-
await tivio.createUserWithEmailAndPassword('test@example.com', 'password', 'Fist & Last Name', '+420777123456')
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Sign in with email and password
|
|
79
|
-
|
|
80
|
-
Returns user's firebase uid or null if error occurs
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
tivio.signInWithEmailAndPassword(email: string, password: string): Promise<string | null>
|
|
84
|
-
```
|
|
85
|
-
**Example**
|
|
86
|
-
```typescript
|
|
87
|
-
await tivio.signInWithEmailAndPassword('test@example.com', 'password')
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Sign out
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
tivio.signOut(): Promise<void>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Reset password
|
|
97
|
-
|
|
98
|
-
Send email with password reset link to the user with given email address
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
tivio.resetPassword(email: string): Promise<void>
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## User entity
|
|
105
|
-
|
|
106
|
-
### Get user
|
|
107
|
-
|
|
108
|
-
Returns user object or null if user is not authenticated
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
tivio.getUser(): Promise<User | null>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Essential Properties
|
|
115
|
-
|
|
116
|
-
#### Basic Information
|
|
117
|
-
- `id: string` - Unique Tivio user ID
|
|
118
|
-
- `email?: string` - Email address
|
|
119
|
-
- `name?: string` - Display name - could be nickname, first name and last name, etc.
|
|
120
|
-
|
|
121
|
-
#### Authentication Status
|
|
122
|
-
- `isSignedIn: boolean` - Whether the user is currently signed in
|
|
123
|
-
- `isReady: boolean` - Whether user data is fully loaded
|
|
124
|
-
- `isAnonymous: boolean` - Whether this is an anonymous user
|
|
125
|
-
|
|
126
|
-
#### Content & Preferences
|
|
127
|
-
- `purchases: Purchase[]` - Active purchases (excluding vouchers)
|
|
128
|
-
|
|
129
|
-
#### User Profiles
|
|
130
|
-
- `profiles: UserProfile[]` - Available user profiles (e.g. multiple people can use the same user account with different preferences)
|
|
131
|
-
- `activeUserProfileId: string | null` - Currently active profile
|
|
132
|
-
|
|
133
|
-
### Essential Methods
|
|
134
|
-
|
|
135
|
-
#### Authentication
|
|
136
|
-
```typescript
|
|
137
|
-
changePassword(oldPassword: string, newPassword: string): Promise<void>
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
#### Profile Management
|
|
141
|
-
|
|
142
|
-
#### Create user profile
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
interface CreateUserProfileRequest {
|
|
146
|
-
/**
|
|
147
|
-
* Profile name - typically first name and last name, but users can fill in whatever they want.
|
|
148
|
-
*/
|
|
149
|
-
name: string
|
|
150
|
-
/**
|
|
151
|
-
* Filled in values for user profile survey. See {@link OrganizationDocument} its configuration.
|
|
152
|
-
*/
|
|
153
|
-
survey?: ProfileSurvey
|
|
154
|
-
}
|
|
155
|
-
interface ProfileSurvey {
|
|
156
|
-
gender?: Translation
|
|
157
|
-
age?: AgeRange
|
|
158
|
-
}
|
|
159
|
-
interface AgeRange {
|
|
160
|
-
from: number
|
|
161
|
-
/**
|
|
162
|
-
* If not set, we assume the range is not closed and {@property to} is set to infinity
|
|
163
|
-
* (there are no values to represent {@link Infinity} in firestore, so we use undefined instead).
|
|
164
|
-
*/
|
|
165
|
-
to?: number
|
|
166
|
-
/**
|
|
167
|
-
* If set, we assume that this is a profile for kids only (e.g. 0-12).
|
|
168
|
-
* This value can only be `true` or not specified (undefined).
|
|
169
|
-
*/
|
|
170
|
-
kidsOnly?: true
|
|
171
|
-
}
|
|
172
|
-
createUserProfile(request: CreateUserProfileRequest): Promise<void>
|
|
173
|
-
```
|
|
174
|
-
**Example**
|
|
175
|
-
```typescript
|
|
176
|
-
await user.createUserProfile({
|
|
177
|
-
name: 'John Doe',
|
|
178
|
-
survey: {
|
|
179
|
-
gender: {
|
|
180
|
-
cs: "Žena",
|
|
181
|
-
en: "Female",
|
|
182
|
-
sk: "Žena"
|
|
183
|
-
},
|
|
184
|
-
age: {
|
|
185
|
-
from: 18,
|
|
186
|
-
to: 24
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
#### Set active user profile
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
user.setActiveUserProfileId(profileId: string): void
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
#### Delete user profile
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
user.deleteUserProfile(profileId: string): Promise<void>
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Content
|
|
205
|
-
|
|
206
|
-
### Tiles and assets
|
|
207
|
-
|
|
208
|
-
A **tile** is a single item rendered inside a content row. The tile is the
|
|
209
|
-
same entity the row points to (a `Video`, `Tag`, `TvChannel`, `Series`,
|
|
210
|
-
`Article` or `Application`), so anything you can read off the entity (name,
|
|
211
|
-
description, ids, monetization flags, …) is also available directly on the
|
|
212
|
-
tile returned by `useItemsInRow` / `row.tiles`.
|
|
213
|
-
|
|
214
|
-
Each tile carries several named image **assets** so the same entity can be
|
|
215
|
-
rendered in different shapes without an extra fetch. Every named getter is a
|
|
216
|
-
shortcut into the underlying `tile.assets` map — use the map directly if you
|
|
217
|
-
need a custom asset that does not have a dedicated getter.
|
|
218
|
-
|
|
219
|
-
**Shared across all entity types**
|
|
220
|
-
|
|
221
|
-
- `tile.landscape` — 16:9 thumbnail (standard rows)
|
|
222
|
-
- `tile.portrait` — 2:3 poster (portrait rows)
|
|
223
|
-
- `tile.circled` — 1:1 circular crop (e.g. creators)
|
|
224
|
-
- `tile.square` — 1:1 square (square rows)
|
|
225
|
-
- `tile.banner` — 16:9 hero banner (banner rows / top of screens)
|
|
226
|
-
- `tile.bannerMobile` — narrow 16:9 banner variant for phones
|
|
227
|
-
- `tile.assets` — raw `{ [assetName]: ScalableAsset }` map behind the getters
|
|
228
|
-
|
|
229
|
-
**Extras by entity type (on top of the shared set above)**
|
|
230
|
-
|
|
231
|
-
- `Video` — `detailBanner` (backdrop above the player), `image`, `cover`
|
|
232
|
-
(deprecated, kept for compatibility), `backgroundBlurBannerMobile`
|
|
233
|
-
- `Tag` / `Article` / `Series` — `detailBanner`, `cover`
|
|
234
|
-
- `TvChannel` — `logo`, `logoPendingOverlayWidth`, `cover`
|
|
235
|
-
- `Application` — read via `tile.application`: `logo`, `logoLandscape`,
|
|
236
|
-
`profilePhoto`
|
|
237
|
-
|
|
238
|
-
If an asset is not set on the entity, the getter returns `null` rather than
|
|
239
|
-
throwing, so it's safe to fall back from a specific variant to a more
|
|
240
|
-
generic one (for example `tile.landscape ?? tile.banner ?? tile.cover`).
|
|
241
|
-
|
|
242
|
-
A live example that renders the same video through every getter is included
|
|
243
|
-
in `examples/sdk-react/src/index.tsx` (`createAssetsShowcaseDemo`).
|
|
244
|
-
|
|
245
|
-
### Get content based on user profile
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
interface GetUserProfileDataOptions {
|
|
249
|
-
/**
|
|
250
|
-
* If true, the data will be returned for all user profiles.
|
|
251
|
-
* If false, the data will be returned only for the active user profile.
|
|
252
|
-
*/
|
|
253
|
-
ignoreActiveUserProfile?: boolean
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
getFavorites(options?: GetUserProfileDataOptions): Promise<FavoriteWithData[]>
|
|
257
|
-
getWatchPositions(options?: GetUserProfileDataOptions): Promise<WatchPositionWithData[]>
|
|
258
|
-
getWatchHistory(options?: GetUserProfileDataOptions): Promise<WatchPositionWithData[]>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Get user favorites
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
interface FavoriteWithData {
|
|
265
|
-
content: Video | Tag
|
|
266
|
-
type: 'video' | 'tag' // Filtering by this type will set the content type to be Video or Tag
|
|
267
|
-
profileId?: string
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
getFavorites(options?: GetUserProfileDataOptions): Promise<FavoriteWithData[]>
|
|
271
|
-
```
|
|
272
|
-
**Example**
|
|
273
|
-
```typescript
|
|
274
|
-
const favorites = await user.getFavorites()
|
|
275
|
-
favorites.forEach(favorite => {
|
|
276
|
-
console.log({
|
|
277
|
-
name: favorite.name,
|
|
278
|
-
cover: favorite.cover, // cover image (landscape)
|
|
279
|
-
portrait: favorite.portrait, // portrait image (portrait)
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Get user watch positions
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
interface WatchPositionWithData {
|
|
288
|
-
position: number
|
|
289
|
-
video: Video
|
|
290
|
-
tag?: Tag
|
|
291
|
-
episodeNumber?: number
|
|
292
|
-
seasonNumber?: number
|
|
293
|
-
videoDuration?: number
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
getWatchPositions(options?: GetUserProfileDataOptions): Promise<WatchPositionWithData[]>
|
|
297
|
-
```
|
|
298
|
-
**Example**
|
|
299
|
-
```typescript
|
|
300
|
-
const watchPositions = await user.getWatchPositions()
|
|
301
|
-
watchPositions.forEach(watchPosition => {
|
|
302
|
-
console.log({
|
|
303
|
-
position: watchPosition.position, // watch position in milliseconds
|
|
304
|
-
videoName: watchPosition.video.name,
|
|
305
|
-
videoCover: watchPosition.video.cover,
|
|
306
|
-
tagName: watchPosition.tag?.name, // optional tag for series
|
|
307
|
-
episodeNumber: watchPosition.episodeNumber, // optional episode number
|
|
308
|
-
seasonNumber: watchPosition.seasonNumber, // optional season number
|
|
309
|
-
videoDuration: watchPosition.videoDuration, // optional video duration
|
|
310
|
-
})
|
|
311
|
-
})
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Get user watch history
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
getWatchHistory(options?: GetUserProfileDataOptions): Promise<WatchPositionWithData[]>
|
|
318
|
-
```
|
|
319
|
-
**Example**
|
|
320
|
-
```typescript
|
|
321
|
-
const watchHistory = await user.getWatchHistory()
|
|
322
|
-
watchHistory.forEach(watchPosition => {
|
|
323
|
-
console.log({
|
|
324
|
-
position: watchPosition.position, // watch position in milliseconds
|
|
325
|
-
videoName: watchPosition.video.name,
|
|
326
|
-
videoCover: watchPosition.video.cover,
|
|
327
|
-
tagName: watchPosition.tag?.name, // optional tag for series
|
|
328
|
-
episodeNumber: watchPosition.episodeNumber, // optional episode number
|
|
329
|
-
seasonNumber: watchPosition.seasonNumber, // optional season number
|
|
330
|
-
videoDuration: watchPosition.videoDuration, // optional video duration
|
|
331
|
-
})
|
|
332
|
-
})
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Add to/remove from favorites
|
|
336
|
-
|
|
337
|
-
Simply call addToFavorites or removeFromFavorites on the Video or Tag.
|
|
338
|
-
```typescript
|
|
339
|
-
const video = await tivio.getVideoById('videoId')
|
|
340
|
-
await video.addToFavorites()
|
|
341
|
-
await video.removeFromFavorites()
|
|
342
|
-
|
|
343
|
-
const tag = await tivio.getTagById('tagId')
|
|
344
|
-
await tag.addToFavorites()
|
|
345
|
-
await tag.removeFromFavorites()
|
|
346
|
-
|
|
347
|
-
// Remove a favorite from favorites
|
|
348
|
-
const favorites = await tivio.getUser()?.favorites
|
|
349
|
-
favorites[0]?.removeFromFavorites()
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Add to/remove from favorites by path
|
|
353
|
-
|
|
354
|
-
You can also add or remove favorites using content paths directly:
|
|
355
|
-
```typescript
|
|
356
|
-
// Add to favorites by path
|
|
357
|
-
await tivio.addToFavoritesByPath('videos/videoId')
|
|
358
|
-
await tivio.addToFavoritesByPath('tags/tagId')
|
|
359
|
-
|
|
360
|
-
// Remove from favorites by path
|
|
361
|
-
await tivio.removeFromFavoritesByPath('videos/videoId')
|
|
362
|
-
await tivio.removeFromFavoritesByPath('tags/tagId')
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
> ℹ️ **_Note:_** When user saves favorite without profileId, it will only be shown if the app doesn't have any active user profile.
|
|
366
|
-
|
|
367
|
-
### Get screen by ID
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
getScreenById(screenId: string): Promise<Screen | null>
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Get row by ID
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
getRowById(rowId: string): Promise<Row | null>
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Get video by ID
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
getVideoById(videoId: string): Promise<Video | null>
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Get tag by ID
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
getTagById(tagId: string): Promise<Tag | null>
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Get TV Channel by ID
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
getTvChannelById(tvChannelId: string): Promise<TvChannel | null>
|
|
395
|
-
```
|
|
396
|
-
|
|
397
66
|
## Player
|
|
398
67
|
|
|
399
68
|
You can choose whether you will use complete player component provided by Tivio or you will wrap your existing player
|
|
@@ -629,64 +298,10 @@ The VideoController returned by `renderWebPlayer` provides the following methods
|
|
|
629
298
|
|
|
630
299
|
The Tivio player supports different types of sources:
|
|
631
300
|
|
|
632
|
-
**PathSourceParams** - Path-based sources with ad configuration
|
|
633
|
-
- Used for playing Tivio-hosted videos and TV channels with custom VAST ad configurations
|
|
634
|
-
- Supports both `videos/ID` and `tvChannels/ID` paths
|
|
635
|
-
- Includes `staticAdsConfig` for custom VAST ad insertion
|
|
636
|
-
- Example usage:
|
|
637
|
-
```typescript
|
|
638
|
-
const sourceWithAds = {
|
|
639
|
-
path: 'videos/123', // or 'tvChannels/456'
|
|
640
|
-
staticAdsConfig: [
|
|
641
|
-
{
|
|
642
|
-
type: 'preroll',
|
|
643
|
-
url: 'https://example.com/vast-preroll-ad.xml',
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
type: 'midroll',
|
|
647
|
-
from: 30000, // 30 seconds
|
|
648
|
-
url: 'https://example.com/vast-midroll-ad.xml',
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
type: 'postroll',
|
|
652
|
-
url: 'https://example.com/vast-postroll-ad.xml',
|
|
653
|
-
},
|
|
654
|
-
],
|
|
655
|
-
}
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
**VOD_TIVIO** - Tivio-hosted video-on-demand content
|
|
659
|
-
- Used for playing videos that are hosted within Tivio's infrastructure
|
|
660
|
-
- Example usage:
|
|
661
|
-
```typescript
|
|
662
|
-
const vodSource = {
|
|
663
|
-
type: SourceType.VOD_TIVIO,
|
|
664
|
-
videoPath: 'videos/123', // Video path
|
|
665
|
-
sourcePlayMode: SourcePlayMode.ON_DEMAND,
|
|
666
|
-
name: 'Tivio Video',
|
|
667
|
-
autoplay: false,
|
|
668
|
-
continuePositionMs: 10000, // Start at 10 seconds (optional)
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**CHANNEL** - TV channel content (both classic and virtual channels)
|
|
673
|
-
- Used for playing live TV channels
|
|
674
|
-
- Example usage:
|
|
675
|
-
```typescript
|
|
676
|
-
const channelSource = {
|
|
677
|
-
type: SourceType.CHANNEL,
|
|
678
|
-
path: 'tvChannels/456', // TV channel path
|
|
679
|
-
sourcePlayMode: SourcePlayMode.LIVE,
|
|
680
|
-
name: 'TV Channel',
|
|
681
|
-
autoplay: true,
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
301
|
**VOD_EXTERNAL** - External video-on-demand content from third-party URLs
|
|
686
302
|
- Used for playing videos that are hosted outside of Tivio's infrastructure
|
|
687
303
|
- Supports various streaming protocols (HLS, DASH, MP4)
|
|
688
|
-
-
|
|
689
|
-
- Example usage (single url):
|
|
304
|
+
- Example usage:
|
|
690
305
|
```typescript
|
|
691
306
|
const externalSource = {
|
|
692
307
|
type: SourceType.VOD_EXTERNAL,
|
|
@@ -696,137 +311,9 @@ const externalSource = {
|
|
|
696
311
|
name: 'External Video',
|
|
697
312
|
autoplay: false,
|
|
698
313
|
continuePositionMs: 10000, // Start at 10 seconds (optional)
|
|
699
|
-
staticAdsConfig: [
|
|
700
|
-
{
|
|
701
|
-
type: 'preroll',
|
|
702
|
-
url: 'https://example.com/preroll-ad.xml',
|
|
703
|
-
},
|
|
704
|
-
{
|
|
705
|
-
type: 'midroll',
|
|
706
|
-
from: 20000, // 20 seconds
|
|
707
|
-
url: 'https://example.com/midroll-ad.xml',
|
|
708
|
-
},
|
|
709
|
-
],
|
|
710
|
-
}
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
**Path-Based Sources**
|
|
714
|
-
You can also use simple path strings for both video and TV channel sources:
|
|
715
|
-
```typescript
|
|
716
|
-
// Video path
|
|
717
|
-
videoController.setSource('videos/123')
|
|
718
|
-
|
|
719
|
-
// TV channel path
|
|
720
|
-
videoController.setSource('tvChannels/456')
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
#### Static Ads Configuration
|
|
724
|
-
|
|
725
|
-
> ℹ️ **_Note:_** To enable ad functionality, you must configure the IMA ad service in your Tivio configuration:
|
|
726
|
-
> ```typescript
|
|
727
|
-
> import { AD_SERVICE_PROXY_NAME } from '@tivio/sdk-react'
|
|
728
|
-
>
|
|
729
|
-
> const config = {
|
|
730
|
-
> // ... other config properties
|
|
731
|
-
> player: {
|
|
732
|
-
> adService: {
|
|
733
|
-
> name: AD_SERVICE_PROXY_NAME.IMA,
|
|
734
|
-
> },
|
|
735
|
-
> },
|
|
736
|
-
> }
|
|
737
|
-
> ```
|
|
738
|
-
|
|
739
|
-
The `staticAdsConfig` property lets you specify custom ad insertion points within your content.
|
|
740
|
-
If multiple ads are set for the same entry point, they will be played one after another in sequence. For example, if you have two preroll ads, the first will play, followed by the second. Similarly, midroll ads that share the same `from` time will be grouped and played sequentially.
|
|
741
|
-
|
|
742
|
-
It supports the following ad types:
|
|
743
|
-
|
|
744
|
-
**Preroll Ads** - Play before the main content starts
|
|
745
|
-
```typescript
|
|
746
|
-
{
|
|
747
|
-
type: 'preroll',
|
|
748
|
-
url: 'https://example.com/preroll-ad.xml',
|
|
749
314
|
}
|
|
750
315
|
```
|
|
751
316
|
|
|
752
|
-
**Midroll Ads** - Play during the main content at specified time
|
|
753
|
-
```typescript
|
|
754
|
-
{
|
|
755
|
-
type: 'midroll',
|
|
756
|
-
from: 30000, // Time in milliseconds (30 seconds)
|
|
757
|
-
url: 'https://example.com/midroll-ad.xml',
|
|
758
|
-
}
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
**Postroll Ads** - Play after the main content ends
|
|
762
|
-
```typescript
|
|
763
|
-
{
|
|
764
|
-
type: 'postroll',
|
|
765
|
-
url: 'https://example.com/postroll-ad.xml',
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
**Complete Example:**
|
|
770
|
-
```typescript
|
|
771
|
-
const sourceWithAds = {
|
|
772
|
-
path: 'videos/123',
|
|
773
|
-
name: 'Video with Multiple Ad Types',
|
|
774
|
-
staticAdsConfig: [
|
|
775
|
-
// multiple preroll ads
|
|
776
|
-
{
|
|
777
|
-
type: 'preroll',
|
|
778
|
-
url: 'https://vasterix.joj.sk/api/v1/creative?id=0c5d96fd-2ab9-4207-a325-4607437965e3&vast=4.0',
|
|
779
|
-
},
|
|
780
|
-
{
|
|
781
|
-
type: 'preroll',
|
|
782
|
-
url: 'https://example.com/preroll-ad.xml',
|
|
783
|
-
},
|
|
784
|
-
// multiple midroll ads (with same from time)
|
|
785
|
-
{
|
|
786
|
-
type: 'midroll',
|
|
787
|
-
from: 30000, // 30 seconds
|
|
788
|
-
url: 'https://example.com/midroll-ad-1.xml',
|
|
789
|
-
},
|
|
790
|
-
{
|
|
791
|
-
type: 'midroll',
|
|
792
|
-
from: 30000, // 30 seconds
|
|
793
|
-
url: 'https://example.com/midroll-ad-2.xml',
|
|
794
|
-
},
|
|
795
|
-
// one midroll ad with different from time
|
|
796
|
-
{
|
|
797
|
-
type: 'midroll',
|
|
798
|
-
from: 60000, // 1 minute
|
|
799
|
-
url: 'https://example.com/midroll-ad-2.xml',
|
|
800
|
-
},
|
|
801
|
-
// postroll ad
|
|
802
|
-
{
|
|
803
|
-
type: 'postroll',
|
|
804
|
-
url: 'https://example.com/postroll-ad.xml',
|
|
805
|
-
},
|
|
806
|
-
],
|
|
807
|
-
}
|
|
808
|
-
```
|
|
809
|
-
- It is also possible to set multiple urls for external source type, e.g. when you want to use both DASH and HLS formats. Player then automatically will pick most suitable format to play depending on device capabilities.
|
|
810
|
-
- Example usage (multiple urls):
|
|
811
|
-
```typescript
|
|
812
|
-
const externalSource = {
|
|
813
|
-
type: SourceType.VOD_EXTERNAL,
|
|
814
|
-
urls: ['https://example.com/video.m3u8', 'https://example.com/video.mpd'],
|
|
815
|
-
sourcePlayMode: SourcePlayMode.ON_DEMAND,
|
|
816
|
-
}
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
**VOD_TIVIO** - Internal source type for playing content managed by Tivio
|
|
820
|
-
- Used for playing videos and tv channels managed by Tivio infrastructure, e.g. everything that is uploaded through Tivio Admin application
|
|
821
|
-
- For convenience, it is recommended to use shortcut format and pass the source as string in `${'videos' | 'tvChannels'}/{id}` format
|
|
822
|
-
- Ids of corresponding videos and tv channel could be found in Tivio admin application
|
|
823
|
-
- Example usage:
|
|
824
|
-
```typescript
|
|
825
|
-
const tivioVideoSource = 'videos/2BzH4xsTXW8vqYKJegXj'
|
|
826
|
-
const tivioTvChannelSource = 'tvChannels/Ct4UcK6ozX3VfxaL5yP5'
|
|
827
|
-
```
|
|
828
|
-
- Otherwise, it is also possible to pass internal tivio source as an object with `type: SourceType.VOD_TIVIO` and additional parameters, similarly to external source type.
|
|
829
|
-
|
|
830
317
|
#### Source Play Modes
|
|
831
318
|
|
|
832
319
|
The `sourcePlayMode` property determines how the content is played:
|
|
@@ -835,72 +322,6 @@ The `sourcePlayMode` property determines how the content is played:
|
|
|
835
322
|
- **LIVE** - Live stream mode with no seeking
|
|
836
323
|
- **HYBRID** - Combines LIVE with seeking
|
|
837
324
|
|
|
838
|
-
#### Player Authentication Configuration
|
|
839
|
-
|
|
840
|
-
Use `disableTvChannelsForAnonymousUsers` property to show overlay to sign in when user is anonymous and tries to play a TV channel (free or monetized).
|
|
841
|
-
Set the property to `true` inside the `player` property in Tivio config.
|
|
842
|
-
|
|
843
|
-
Tivio config:
|
|
844
|
-
```typescript
|
|
845
|
-
const config = {
|
|
846
|
-
// ... other config properties
|
|
847
|
-
player: {
|
|
848
|
-
// ... other player properties
|
|
849
|
-
disableTvChannelsForAnonymousUsers: true, // or false
|
|
850
|
-
},
|
|
851
|
-
}
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
#### User Authentication Callbacks
|
|
855
|
-
|
|
856
|
-
The `userAuthCallbacks` property allows you to handle user authentication flows when the player requires user login or registration. This is particularly useful for content that requires authentication (e.g., premium content).
|
|
857
|
-
|
|
858
|
-
```typescript
|
|
859
|
-
interface UserAuthCallbacks {
|
|
860
|
-
onGoToLogin: () => void
|
|
861
|
-
onGoToRegistration: () => void
|
|
862
|
-
}
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
**Example Implementation:**
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
// Usage with renderWebPlayer
|
|
869
|
-
const videoController = await renderWebPlayer(
|
|
870
|
-
document.getElementById('video-player'),
|
|
871
|
-
{
|
|
872
|
-
id: 'player-main',
|
|
873
|
-
source: 'videos/PREMIUM_VIDEO_ID',
|
|
874
|
-
userAuthCallbacks: {
|
|
875
|
-
onGoToLogin: () => {
|
|
876
|
-
// Show your login modal
|
|
877
|
-
setShowLoginModal(true)
|
|
878
|
-
},
|
|
879
|
-
onGoToRegistration: () => {
|
|
880
|
-
// Show your registration modal
|
|
881
|
-
setShowRegistrationModal(true)
|
|
882
|
-
},
|
|
883
|
-
},
|
|
884
|
-
}
|
|
885
|
-
)
|
|
886
|
-
|
|
887
|
-
// Handle login in your modal
|
|
888
|
-
const handleLogin = async (email: string, password: string) => {
|
|
889
|
-
try {
|
|
890
|
-
await tivio.signInWithEmailAndPassword(email, password)
|
|
891
|
-
console.log('Login successful')
|
|
892
|
-
} catch (error) {
|
|
893
|
-
console.error('Login failed:', error)
|
|
894
|
-
throw error
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
**When are these callbacks triggered?**
|
|
900
|
-
|
|
901
|
-
- **`onGoToLogin`**: Called when the player is trying to play content behind a paywall and user clicks on the login button in the overlay
|
|
902
|
-
- **`onGoToRegistration`**: Called when the player is trying to play content behind a paywall and user clicks on the registration button in the overlay
|
|
903
|
-
|
|
904
325
|
#### Using setSource with VideoController
|
|
905
326
|
|
|
906
327
|
The `setSource` method allows you to dynamically change what's playing:
|
|
@@ -909,23 +330,6 @@ The `setSource` method allows you to dynamically change what's playing:
|
|
|
909
330
|
// Change to a different video
|
|
910
331
|
videoController.setSource('videos/newVideoId')
|
|
911
332
|
|
|
912
|
-
// Change to a video with custom ads
|
|
913
|
-
videoController.setSource({
|
|
914
|
-
path: 'videos/newVideoId',
|
|
915
|
-
name: 'Video with Ads',
|
|
916
|
-
staticAdsConfig: [
|
|
917
|
-
{
|
|
918
|
-
type: 'preroll',
|
|
919
|
-
url: 'https://example.com/preroll-ad.xml',
|
|
920
|
-
},
|
|
921
|
-
{
|
|
922
|
-
type: 'midroll',
|
|
923
|
-
from: 30000,
|
|
924
|
-
url: 'https://example.com/midroll-ad.xml',
|
|
925
|
-
},
|
|
926
|
-
],
|
|
927
|
-
})
|
|
928
|
-
|
|
929
333
|
// Change to an external video
|
|
930
334
|
videoController.setSource({
|
|
931
335
|
type: SourceType.VOD_EXTERNAL,
|
|
@@ -937,18 +341,6 @@ videoController.setSource({
|
|
|
937
341
|
// Change to a TV channel
|
|
938
342
|
videoController.setSource('tvChannels/channelId')
|
|
939
343
|
|
|
940
|
-
// Change to a TV channel with custom ads
|
|
941
|
-
videoController.setSource({
|
|
942
|
-
path: 'tvChannels/channelId',
|
|
943
|
-
name: 'TV Channel with Ads',
|
|
944
|
-
staticAdsConfig: [
|
|
945
|
-
{
|
|
946
|
-
type: 'preroll',
|
|
947
|
-
url: 'https://example.com/tv-preroll-ad.xml',
|
|
948
|
-
},
|
|
949
|
-
],
|
|
950
|
-
})
|
|
951
|
-
|
|
952
344
|
// Stop playback
|
|
953
345
|
videoController.setSource(null)
|
|
954
346
|
```
|
|
@@ -958,238 +350,17 @@ The `setSource` method is particularly useful for:
|
|
|
958
350
|
- Implementing playlists
|
|
959
351
|
- Dynamic content loading
|
|
960
352
|
|
|
961
|
-
#### Minimal OSD / Sticky Mini Player
|
|
962
|
-
|
|
963
|
-
The player supports a minimal OSD mode designed for small or pinned containers such as a
|
|
964
|
-
scroll-fixed "mini" player. When enabled, only the essential controls are rendered:
|
|
965
|
-
|
|
966
|
-
- `play` / `pause` (small button in the bottom-left corner)
|
|
967
|
-
- `volume`
|
|
968
|
-
- `fullscreen`
|
|
969
|
-
|
|
970
|
-
The mode can be toggled in two ways:
|
|
971
|
-
|
|
972
|
-
1. Declaratively, via the `isMinimal` prop on `WebPlayerProps`.
|
|
973
|
-
2. Imperatively, via the vanilla controller's `setIsMinimal(isMinimal: boolean)` method — useful
|
|
974
|
-
for switching on/off at runtime (e.g. when the player becomes pinned after the user scrolls
|
|
975
|
-
past it).
|
|
976
|
-
|
|
977
|
-
**Example: scroll-pinned mini player**
|
|
978
|
-
|
|
979
|
-
The layout is driven entirely by CSS — the JS only toggles an `is-floating` class and calls
|
|
980
|
-
`setIsMinimal(true|false)` accordingly. Responsive behavior (desktop corner vs. mobile
|
|
981
|
-
top-full-width) is handled with a media query, so there is no need to watch viewport size from
|
|
982
|
-
React/JS.
|
|
983
|
-
|
|
984
|
-
```html
|
|
985
|
-
<section class="mini-player-demo">
|
|
986
|
-
<div class="mini-player-anchor">
|
|
987
|
-
<div class="mini-player" id="mini-player"></div>
|
|
988
|
-
</div>
|
|
989
|
-
<!-- ...page content... -->
|
|
990
|
-
</section>
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
```css
|
|
994
|
-
.mini-player-anchor {
|
|
995
|
-
position: relative;
|
|
996
|
-
width: 100%;
|
|
997
|
-
aspect-ratio: 16 / 9;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
.mini-player {
|
|
1001
|
-
position: absolute;
|
|
1002
|
-
inset: 0;
|
|
1003
|
-
background: #000;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/* Floating (desktop): pinned to bottom-right. */
|
|
1007
|
-
.mini-player.is-floating {
|
|
1008
|
-
position: fixed;
|
|
1009
|
-
inset: auto 24px 24px auto;
|
|
1010
|
-
width: 360px;
|
|
1011
|
-
aspect-ratio: 16 / 9;
|
|
1012
|
-
z-index: 1000;
|
|
1013
|
-
border-radius: 8px;
|
|
1014
|
-
overflow: hidden;
|
|
1015
|
-
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/* Floating (narrow viewports): pinned to top, full width. */
|
|
1019
|
-
@media (max-width: 600px) {
|
|
1020
|
-
.mini-player.is-floating {
|
|
1021
|
-
inset: 0 0 auto 0;
|
|
1022
|
-
width: 100%;
|
|
1023
|
-
border-radius: 0;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
```
|
|
1027
|
-
|
|
1028
|
-
```ts
|
|
1029
|
-
import { renderWebPlayer } from '@tivio/sdk-react'
|
|
1030
|
-
|
|
1031
|
-
const container = document.getElementById('mini-player')!
|
|
1032
|
-
const anchor = document.querySelector('.mini-player-anchor')!
|
|
1033
|
-
|
|
1034
|
-
const controller = await renderWebPlayer(container, {
|
|
1035
|
-
id: 'mini-player',
|
|
1036
|
-
source: 'videos/YOUR_VIDEO_ID',
|
|
1037
|
-
isSameSizeAsParent: true,
|
|
1038
|
-
isMutedByDefault: true,
|
|
1039
|
-
autoplay: true,
|
|
1040
|
-
})
|
|
1041
|
-
|
|
1042
|
-
// Pop out of the flow once the inline anchor leaves the viewport,
|
|
1043
|
-
// and switch the OSD between minimal and default accordingly.
|
|
1044
|
-
new IntersectionObserver(
|
|
1045
|
-
([entry]) => {
|
|
1046
|
-
const isFloating = !entry.isIntersecting
|
|
1047
|
-
container.classList.toggle('is-floating', isFloating)
|
|
1048
|
-
controller.setIsMinimal(isFloating)
|
|
1049
|
-
},
|
|
1050
|
-
{ threshold: 0.1 },
|
|
1051
|
-
).observe(anchor)
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
Notes:
|
|
1055
|
-
- The anchor stays in the flow with its original 16:9 size, so the page layout does not jump
|
|
1056
|
-
when the player detaches.
|
|
1057
|
-
- The desktop/mobile floating layout is fully CSS-driven; resizing the browser window
|
|
1058
|
-
updates it live with no extra JS.
|
|
1059
|
-
|
|
1060
|
-
You can see this pattern in action in `examples/sdk-react/src/index.tsx` (the
|
|
1061
|
-
`createStickyMiniPlayerDemo` function) with styles in `examples/sdk-react/src/index.css`.
|
|
1062
|
-
|
|
1063
353
|
**Event Handling:**
|
|
1064
354
|
- `addEventListener(event: string, callback: (value: T) => void)` - Add event listener
|
|
1065
355
|
- `removeEventListener(event: string, callback: (value: T) => void)` - Remove event listener
|
|
1066
356
|
|
|
1067
357
|
**Utility:**
|
|
1068
|
-
- `setIsMinimal(isMinimal: boolean)` - Toggle the minimal OSD mode at runtime (see
|
|
1069
|
-
[Minimal OSD / Sticky Mini Player](#minimal-osd--sticky-mini-player))
|
|
1070
358
|
- `destroy()` - Destroy player and clean up resources
|
|
1071
359
|
|
|
1072
360
|
#### Playback Events
|
|
1073
361
|
|
|
1074
362
|
The VideoController emits various events that you can listen to:
|
|
1075
363
|
|
|
1076
|
-
#### Ad events
|
|
1077
|
-
|
|
1078
|
-
```typescript
|
|
1079
|
-
videoController.addEventListener('ad-started', (adMetadata: AdMetadata | null) => {
|
|
1080
|
-
if (!adMetadata) {
|
|
1081
|
-
console.log('Ad started playing (no metadata available)')
|
|
1082
|
-
return
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
console.log('Ad started playing', adMetadata)
|
|
1086
|
-
|
|
1087
|
-
if ('customAdMetadata' in adMetadata && adMetadata.customAdMetadata) {
|
|
1088
|
-
console.log('Ad custom metadata:', adMetadata.customAdMetadata)
|
|
1089
|
-
// customAdMetadata contains VAST trafficking parameters (from VAST AdParameters tag)
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// Access CTA element for rendering custom call-to-action buttons
|
|
1093
|
-
const { ctaElement } = adMetadata
|
|
1094
|
-
if (ctaElement) {
|
|
1095
|
-
console.log('CTA element available for rendering custom buttons', ctaElement)
|
|
1096
|
-
|
|
1097
|
-
const { customAdMetadata } = adMetadata
|
|
1098
|
-
if (!customAdMetadata) {
|
|
1099
|
-
console.log('No custom ad metadata available')
|
|
1100
|
-
return
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const { extensions } = customAdMetadata
|
|
1104
|
-
if (!extensions) {
|
|
1105
|
-
console.log('No extensions available')
|
|
1106
|
-
return
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const { parameters } = extensions[0]
|
|
1110
|
-
if (!parameters) {
|
|
1111
|
-
console.log('No parameters available')
|
|
1112
|
-
return
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const metadataParameters = parameters as {
|
|
1116
|
-
main_title?: string
|
|
1117
|
-
subtitle?: string
|
|
1118
|
-
image?: string
|
|
1119
|
-
button_text: string
|
|
1120
|
-
url: string
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const buttonText = metadataParameters.button_text as string | undefined
|
|
1124
|
-
|
|
1125
|
-
if (!buttonText) {
|
|
1126
|
-
console.log('No button text available')
|
|
1127
|
-
return
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Example: Create a custom CTA button using React portal
|
|
1131
|
-
const CTAButton = () => (
|
|
1132
|
-
<div style={{
|
|
1133
|
-
position: 'absolute',
|
|
1134
|
-
bottom: '20px',
|
|
1135
|
-
right: '20px',
|
|
1136
|
-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
1137
|
-
color: 'white',
|
|
1138
|
-
padding: '12px 24px',
|
|
1139
|
-
borderRadius: '6px',
|
|
1140
|
-
cursor: 'pointer',
|
|
1141
|
-
fontSize: '16px',
|
|
1142
|
-
fontWeight: 'bold',
|
|
1143
|
-
pointerEvents: 'auto',
|
|
1144
|
-
}}>
|
|
1145
|
-
Learn More
|
|
1146
|
-
</div>
|
|
1147
|
-
)
|
|
1148
|
-
|
|
1149
|
-
// Render the CTA button using React portal
|
|
1150
|
-
const root = ReactDOM.createRoot(ctaElement)
|
|
1151
|
-
root.render(<CTAButton />)
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// adMetadata contains information like:
|
|
1155
|
-
// - ctaElement?: HTMLElement (for rendering custom CTA buttons)
|
|
1156
|
-
// - customAdMetadata?: Record<string, unknown> (for IMA ads with rich metadata)
|
|
1157
|
-
// - type: 'ad'
|
|
1158
|
-
// - subType: 'inserted' | 'original'
|
|
1159
|
-
// - secondsToEnd: number
|
|
1160
|
-
// - secondsToSkippable: number | null
|
|
1161
|
-
// - canTriggerSkip: boolean
|
|
1162
|
-
// - isSkippable: boolean
|
|
1163
|
-
// - order: number | null
|
|
1164
|
-
// - totalCount: number | null
|
|
1165
|
-
// - skip: () => void
|
|
1166
|
-
// Update UI, show ad overlay, etc.
|
|
1167
|
-
})
|
|
1168
|
-
|
|
1169
|
-
videoController.addEventListener('ad-ended', () => {
|
|
1170
|
-
console.log('Ad finished playing')
|
|
1171
|
-
})
|
|
1172
|
-
|
|
1173
|
-
// Companion ads event - fires when companion ads are available for the current ad
|
|
1174
|
-
videoController.addEventListener('companion-ads', (companionAds) => {
|
|
1175
|
-
console.log('Companion ads available:', companionAds)
|
|
1176
|
-
|
|
1177
|
-
// Access companion ad properties using IMA methods
|
|
1178
|
-
companionAds.forEach((companionAd, index) => {
|
|
1179
|
-
console.log(`Companion Ad ${index + 1}:`, {
|
|
1180
|
-
width: companionAd.getWidth(),
|
|
1181
|
-
height: companionAd.getHeight(),
|
|
1182
|
-
contentType: companionAd.getContentType(),
|
|
1183
|
-
// HTML content as string
|
|
1184
|
-
content: companionAd.getContent()
|
|
1185
|
-
})
|
|
1186
|
-
})
|
|
1187
|
-
})
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
> ℹ️ **_Note:_** The CTA overlay element is visible in the WebPlayer only while an ad is playing and is automatically cleaned up on source changes.
|
|
1191
|
-
|
|
1192
|
-
#### Video state changes
|
|
1193
364
|
```typescript
|
|
1194
365
|
// Video state changes
|
|
1195
366
|
videoController.addEventListener('statechange', (state) => {
|
|
@@ -1267,11 +438,6 @@ The `WebPlayerProps` interface defines the properties that can be passed to the
|
|
|
1267
438
|
- **`showTvStreamType`** (optional): Whether to show TV stream type indicator
|
|
1268
439
|
- **`showCookiesSettings`** (optional): Whether to show cookies settings
|
|
1269
440
|
- **`showOsd`** (optional, default: `true`): Whether to show the On-Screen Display (OSD)
|
|
1270
|
-
- **`isMinimal`** (optional, default: `false`): If `true`, the player uses a minimal OSD that
|
|
1271
|
-
exposes only `play`/`pause`, `volume` and `fullscreen` controls. A small center play/pause
|
|
1272
|
-
icon flashes briefly when the state toggles and then fades out.
|
|
1273
|
-
Designed for small/pinned containers such as scroll-fixed mini players. See
|
|
1274
|
-
[Minimal OSD / Sticky Mini Player](#minimal-osd--sticky-mini-player) for usage details.
|
|
1275
441
|
- **`showBufferingSpinner`** (optional, default: `true`): Whether to show buffering spinner
|
|
1276
442
|
|
|
1277
443
|
### Audio Properties
|
|
@@ -1282,7 +448,7 @@ The `WebPlayerProps` interface defines the properties that can be passed to the
|
|
|
1282
448
|
|
|
1283
449
|
- **`enableKeyboardShortcuts`** (optional, default: `true`): Whether to enable keyboard shortcuts
|
|
1284
450
|
- **`customShortcuts`** (optional): Custom keyboard shortcuts configuration:
|
|
1285
|
-
```typescript
|
|
451
|
+
```typescript
|
|
1286
452
|
{
|
|
1287
453
|
toggleFullscreen: number[], // Array of key codes
|
|
1288
454
|
togglePause: number[],
|
|
@@ -1292,7 +458,7 @@ The `WebPlayerProps` interface defines the properties that can be passed to the
|
|
|
1292
458
|
volumeUp: number[],
|
|
1293
459
|
volumeDown: number[]
|
|
1294
460
|
}
|
|
1295
|
-
```
|
|
461
|
+
```
|
|
1296
462
|
|
|
1297
463
|
### Ad Block Properties
|
|
1298
464
|
|