diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index d5db1ed05..5635d0e3a 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -69,7 +69,7 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
- - uses: gradle/wrapper-validation-action@v1
+ - uses: gradle/wrapper-validation-action@v2
- uses: gradle/actions/setup-gradle@v3
with:
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7cb658a5d..bdaa3dca1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
VERSION_NAME: ${{ github.ref_name }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Check if pre release tag
id: check-tag
run: |
@@ -27,7 +27,7 @@ jobs:
echo "tag = ${GITHUB_REF_NAME}"
echo "version_name = ${VERSION_NAME}"
- name: Set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml
deleted file mode 100644
index 1ff683d26..000000000
--- a/.idea/kotlinScripting.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- 2
-
-
- 0
-
-
- 1
-
-
- 3
-
-
- 4
-
-
- 5
-
-
-
diff --git a/build.gradle.kts b/build.gradle.kts
index a4e1981ed..f7cc0e466 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -101,6 +101,20 @@ dependencyAnalysis {
}
}
+ project(":pillarbox-demo") {
+ onUnusedDependencies {
+ // This dependency is not used directly, but required to have previews in Android Studio
+ exclude(libs.androidx.compose.ui.tooling.asProvider())
+ }
+ }
+
+ project(":pillarbox-demo-tv") {
+ onUnusedDependencies {
+ // This dependency is not used directly, but required to have previews in Android Studio
+ exclude(libs.androidx.compose.ui.tooling.asProvider())
+ }
+ }
+
project(":pillarbox-player") {
onUnusedDependencies {
// These dependencies are not used directly, but automatically used by libs.androidx.media3.exoplayer
@@ -109,5 +123,12 @@ dependencyAnalysis {
exclude(libs.mockk.android)
}
}
+
+ project(":pillarbox-ui") {
+ onUnusedDependencies {
+ // This dependency is not used directly, but required to have previews in Android Studio
+ exclude(libs.androidx.compose.ui.tooling.asProvider())
+ }
+ }
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0f18e8959..cdcae0757 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,11 +1,11 @@
[versions]
accompanist = "0.34.0"
-android-gradle-plugin = "8.2.2"
+android-gradle-plugin = "8.3.0"
androidx-activity = "1.8.2"
androidx-annotation = "1.7.1"
-androidx-compose = "2024.02.00"
+androidx-compose = "2024.02.01"
# https://developer.android.com/jetpack/androidx/releases/compose-kotlin
-androidx-compose-compiler = "1.5.9"
+androidx-compose-compiler = "1.5.10"
androidx-core = "1.12.0"
androidx-fragment = "1.6.2"
androidx-leanback = "1.0.0"
@@ -21,20 +21,20 @@ androidx-test-runner = "1.5.2"
androidx-tv = "1.0.0-alpha10"
coil = "2.5.0"
comscore = "6.10.0"
-dependency-analysis-gradle-plugin = "1.29.0"
+dependency-analysis-gradle-plugin = "1.30.0"
detekt = "1.23.5"
guava = "31.1-android"
-json = "20231013"
+json = "20240205"
junit = "4.13.2"
kotlin = "1.9.22"
kotlinx-coroutines = "1.8.0"
kotlinx-kover = "0.7.6"
-kotlinx-serialization = "1.6.2"
-ktor = "2.3.8"
-mockk = "1.13.9"
+kotlinx-serialization = "1.6.3"
+ktor = "2.3.9"
+mockk = "1.13.10"
okhttp = "4.12.0"
robolectric = "4.11.1"
-srg-data-provider = "0.8.0"
+srg-data-provider = "0.8.2"
tag-commander-core = "5.4.3"
tag-commander-server-side = "5.5.2"
turbine = "1.0.0"
diff --git a/pillarbox-core-business/build.gradle.kts b/pillarbox-core-business/build.gradle.kts
index 3e6834564..2f03aad46 100644
--- a/pillarbox-core-business/build.gradle.kts
+++ b/pillarbox-core-business/build.gradle.kts
@@ -100,15 +100,7 @@ dependencies {
testImplementation(libs.mockk.dsl)
testRuntimeOnly(libs.robolectric)
testImplementation(libs.robolectric.annotations)
- testRuntimeOnly(libs.robolectric.shadows.framework)
-
- androidTestImplementation(project(":pillarbox-player-testutils"))
-
- androidTestImplementation(libs.androidx.test.monitor)
- androidTestImplementation(libs.androidx.test.runner)
- androidTestImplementation(libs.junit)
- androidTestRuntimeOnly(libs.kotlinx.coroutines.android)
- androidTestImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(libs.robolectric.shadows.framework)
}
koverReport {
diff --git a/pillarbox-core-business/src/androidTest/assets/media-compositions.json b/pillarbox-core-business/src/androidTest/assets/media-compositions.json
deleted file mode 100644
index 31bc396be..000000000
--- a/pillarbox-core-business/src/androidTest/assets/media-compositions.json
+++ /dev/null
@@ -1,1521 +0,0 @@
-[
- {
- "chapterUrn": "urn:rts:audio:3262363",
- "episode": {
- "id": "3262367",
- "title": "Couleur 3 en direct",
- "publishedDate": "2011-07-11T14:20:07+02:00",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3"
- },
- "show": {
- "id": "3262370",
- "vendor": "RTS",
- "transmission": "RADIO",
- "urn": "urn:rts:show:radio:3262370",
- "title": "Couleur 3 en direct",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "bannerImageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/3x1",
- "posterImageUrl": "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg",
- "posterImageIsFallbackUrl": true,
- "primaryChannelId": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "primaryChannelUrn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "audioDescriptionAvailable": false,
- "subtitlesAvailable": false,
- "multiAudioLanguagesAvailable": false,
- "allowIndexing": false
- },
- "channel": {
- "id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "vendor": "RTS",
- "urn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "title": "Couleur 3",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "transmission": "RADIO"
- },
- "chapterList": [
- {
- "id": "3262363",
- "mediaType": "AUDIO",
- "vendor": "RTS",
- "urn": "urn:rts:audio:3262363",
- "title": "Couleur 3 en direct",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "type": "LIVESTREAM",
- "date": "2011-07-11T14:20:07+02:00",
- "duration": 0,
- "playableAbroad": true,
- "displayable": true,
- "position": 0,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Livestream",
- "media_type": "Audio",
- "media_segment_id": "3262363",
- "media_episode_length": "0",
- "media_segment_length": "0",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "1",
- "media_duration_category": "infinit.livestream",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:audio:3262363"
- },
- "fullLengthMarkIn": 0,
- "fullLengthMarkOut": 0,
- "resourceList": [
- {
- "url": "http://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8?",
- "quality": "HD",
- "protocol": "HLS-DVR",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": true,
- "live": true,
- "mediaContainer": "MPEG2_TS",
- "audioCodec": "AAC",
- "videoCodec": "NONE",
- "tokenType": "NONE",
- "analyticsMetadata": {
- "media_streaming_quality": "HD",
- "media_special_format": "DEFAULT",
- "media_url": "http://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8?"
- },
- "streamOffset": 55000
- }
- ]
- }
- ],
- "analyticsData": {
- "srg_pr_id": "3262367",
- "srg_plid": "3262370",
- "ns_st_pl": "Livestream",
- "ns_st_pr": "Couleur 3 en direct",
- "ns_st_dt": "2011-07-11",
- "ns_st_ddt": "2011-07-11",
- "ns_st_tdt": "2011-07-11",
- "ns_st_tm": "14:20:07",
- "ns_st_tep": "*null",
- "ns_st_li": "1",
- "ns_st_stc": "0867",
- "ns_st_st": "Couleur 3",
- "ns_st_tpr": "11562086",
- "ns_st_en": "*null",
- "ns_st_ge": "*null",
- "ns_st_ia": "*null",
- "ns_st_ce": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc",
- "srg_unit": "RTS",
- "srg_c1": "live",
- "srg_c2": "rts.ch_audio_couleur3",
- "srg_c3": "COULEUR 3",
- "srg_aod_prid": "3262367"
- },
- "analyticsMetadata": {
- "media_episode_id": "3262367",
- "media_show_id": "11562086",
- "media_show": "Oui Mais Non",
- "media_episode": "Couleur 3 en direct",
- "media_is_livestream": "true",
- "media_full_length": "full",
- "media_enterprise_units": "RTS",
- "media_joker1": "live",
- "media_joker2": "rts.ch_audio_couleur3",
- "media_joker3": "COULEUR 3",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_thumbnail": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9/scale/width/344",
- "media_publication_date": "2011-07-11",
- "media_publication_time": "14:20:07",
- "media_publication_datetime": "2011-07-11T14:20:07+02:00",
- "media_tv_date": "2011-07-11",
- "media_tv_time": "14:20:07",
- "media_tv_datetime": "2011-07-11T14:20:07+02:00",
- "media_content_group": "Couleur 3",
- "media_channel_id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "media_channel_cs": "0867",
- "media_channel_name": "Couleur 3",
- "media_since_publication_d": "4322",
- "media_since_publication_h": "103747"
- }
- },
- {
- "chapterUrn": "urn:rts:video:6820736",
- "episode": {
- "id": "6703608",
- "title": "Le 19h30",
- "publishedDate": "2015-05-28T19:30:00+02:00",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820735.image/16x9",
- "imageTitle": "Le 19h30 [RTS]"
- },
- "show": {
- "id": "105932",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:show:tv:105932",
- "title": "19h30",
- "lead": "L'édition du soir du téléjournal.",
- "imageUrl": "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9",
- "imageTitle": "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]",
- "bannerImageUrl": "https://www.rts.ch/2019/08/28/11/33/10667272.image/3x1",
- "posterImageUrl": "https://www.rts.ch/2021/08/05/18/12/12396566.image/2x3",
- "posterImageIsFallbackUrl": false,
- "primaryChannelId": "143932a79bb5a123a646b68b1d1188d7ae493e5b",
- "primaryChannelUrn": "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b",
- "availableAudioLanguageList": [
- {
- "locale": "fr",
- "language": "Français"
- }
- ],
- "availableVideoQualityList": [
- "SD",
- "HD"
- ],
- "audioDescriptionAvailable": false,
- "subtitlesAvailable": true,
- "multiAudioLanguagesAvailable": false,
- "topicList": [
- {
- "id": "908",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:908",
- "title": "19h30"
- },
- {
- "id": "904",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:904",
- "title": "Vidéos"
- },
- {
- "id": "665",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:665",
- "title": "Info"
- }
- ],
- "allowIndexing": false
- },
- "channel": {
- "id": "143932a79bb5a123a646b68b1d1188d7ae493e5b",
- "vendor": "RTS",
- "urn": "urn:rts:channel:tv:143932a79bb5a123a646b68b1d1188d7ae493e5b",
- "title": "RTS 1",
- "imageUrl": "https://www.rts.ch/2019/08/28/11/33/10667272.image/16x9",
- "imageUrlRaw": "https://il.srgssr.ch/image-service/dynamic/8eebe5.svg",
- "imageTitle": "RTS Info - Le 19h30, avec nouveau logo RTS Info (la mise en ligne le lundi 26 août 2019) [RTS]",
- "transmission": "TV"
- },
- "chapterList": [
- {
- "id": "6820736",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820736",
- "title": "Le 19h30",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820735.image/16x9",
- "imageTitle": "Le 19h30 [RTS]",
- "type": "EPISODE",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 1897960,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "socialCountList": [
- {
- "key": "srgView",
- "value": 4731
- },
- {
- "key": "srgLike",
- "value": 0
- },
- {
- "key": "fbShare",
- "value": 4
- },
- {
- "key": "twitterShare",
- "value": 1
- },
- {
- "key": "googleShare",
- "value": 0
- },
- {
- "key": "whatsAppShare",
- "value": 1
- }
- ],
- "displayable": true,
- "position": 0,
- "noEmbed": false,
- "analyticsData": {
- "ns_st_ep": "Le 19h30",
- "ns_st_ty": "Video",
- "ns_st_ci": "6820736",
- "ns_st_el": "1897960",
- "ns_st_cl": "1897960",
- "ns_st_sl": "1897960",
- "srg_mgeobl": "false",
- "ns_st_tp": "12",
- "ns_st_cn": "1",
- "ns_st_ct": "vc12",
- "ns_st_pn": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc"
- },
- "analyticsMetadata": {
- "media_segment": "Le 19h30",
- "media_type": "Video",
- "media_segment_id": "6820736",
- "media_episode_length": "1898",
- "media_segment_length": "1898",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "12",
- "media_duration_category": "long",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820736"
- },
- "eventData": "$35972c05ea85ec0d$1096eaf8d14e46b93b68493a1eb04f82b572de863dbcd5e21a05d5da2b8fb4067c33aa0fb0b8e4fc268fbc0e79b81da7ff672aab0df30aa010068e4af06479cd74dc44a935a85aa90d4b7645bce56033a2bb43fd7541aea064a7dd955fbb26d7d11d05b93b91c91e4f6c52d669beda436c9512336065aba0606a14147766aefc1133c4f082a561abea722fb48fa5131b00d0c4a3739533969bc16a812df9172241f76ad7124db467dc988aaac6660bcc942c722bed902a97c5d6c489d9879b334c2cbe89d70784dcd188591ef0e9f2cab5de79d54fa54ec9e291cdd67bf91b1ebbdde8e9a1a10df8549e5abd3f4fafef5adfba535c8ea5d8ee12d41e8e293b2374416b44c9b53eb2c9effde399f7fd0797040ccbecfb2200e519bacc0f90fbf9799369cbb48222acc6f243d665209ce1e19ef4d4cb670333139fd1bd3a16191f3faa8ef35abd3d4e87f16d7554b2779abbe3ced7eb059aca7efe880d583081a769cb6229c8b012d8db66c2e9ef79b2bc",
- "resourceList": [
- {
- "url": "https://rts-vod-amd.akamaized.net/ww/6820736/44612b73-9114-3968-b712-472a2d10cbad/master.m3u8",
- "quality": "HD",
- "protocol": "HLS",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": false,
- "live": false,
- "mediaContainer": "FMP4",
- "audioCodec": "AAC",
- "videoCodec": "H264",
- "tokenType": "NONE",
- "audioTrackList": [
- {
- "locale": "fr",
- "language": "Français",
- "source": "HLS"
- }
- ],
- "subtitleInformationList": [
- {
- "locale": "fr",
- "language": "Français (SDH)",
- "source": "HLS",
- "type": "SDH"
- }
- ],
- "analyticsData": {
- "srg_mqual": "HD",
- "srg_mpres": "DEFAULT"
- },
- "analyticsMetadata": {
- "media_streaming_quality": "HD",
- "media_special_format": "DEFAULT",
- "media_url": "https://rts-vod-amd.akamaized.net/ww/6820736/44612b73-9114-3968-b712-472a2d10cbad/master.m3u8"
- }
- }
- ],
- "segmentList": [
- {
- "id": "6820712",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820712",
- "title": "FIFA: Sepp Blatter se veut distant des corrompus présumés",
- "description": "Le président de l'organisation s'est exprimé et a affirmé ne pas pouvoir \"surveiller tout le monde\".",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820705.image/16x9",
- "imageTitle": "FIFA: Sepp Blatter se veut distant des corrompus présumés [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 100200,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 1,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: Sepp Blatter se veut distant des corrompus présumés",
- "media_type": "Video",
- "media_segment_id": "6820712",
- "media_episode_length": "1898",
- "media_segment_length": "100",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820712"
- },
- "eventData": "$71688688204e7d09$9c0f5dd7f150d9f99795006a27880cc17b0ab91a0e2e3fdb73192f74a16e73f3bdb7815f9d45fd71a84cfb497606dd6f84da95013d0aa37b798ef97b25ce032b5ac23ec4f97b7b75b706e060a7e2c0219687cf0a77010b1ff58cccaf66b639dba8dc3087428271cc1fcc0dfc00c0262d586c1fe676f85b41a600ab94da981051a43313905a212fef157dacf7b373465fe5715073d4b35cd56fbbf4c7a621433114a1b93a65109f8a09055ee6a492f3605d8f001297cbe1eaa6e237bc1ccf3f802f903b427a1f7728e9862fb8b03011aba8e58562a4e55da17bd19d934f9a62a32eb13ebaff59848d93e2097dcc5c4fc2b511c9f23baf152f6ddde4f9bd6339dacfa77df2b59f52aa3b8545d6acb7ea6ee64e8517d407ef9416be9cfe86d2b5c5255d2e88777067cde8794444b9619a0cf93b0196d011eaff6aabaf9af402e901d69ef0fedfa04ed3c5e366ea96f2a9d7edf3c9bcd079a04808a65342bfb4439ee4d7c5ffba9989682205c0fc3eeeb334",
- "markIn": 111320,
- "markOut": 211520
- },
- {
- "id": "6820720",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820720",
- "title": "FIFA: nombreux souhaitent que Blatter ne soit pas réélu",
- "description": "Ces élections présidentielles, qui opposent le prince Ali et le Haut-Valaisan, aura lieu à la date prévue malgré tout.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820719.image/16x9",
- "imageTitle": "FIFA: nombreux souhaitent que Blatter ne soit pas réélu [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 138720,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 2,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: nombreux souhaitent que Blatter ne soit pas réélu",
- "media_type": "Video",
- "media_segment_id": "6820720",
- "media_episode_length": "1898",
- "media_segment_length": "139",
- "media_number_of_segment_selected": "2",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820720"
- },
- "eventData": "$ed7abc075aac5be8$70d739558987bce0b0491f7d7b41273f7daa9711bb3cae813afb17f85d342e848ad8d2154f43d27e3e72c07d147e9e8e96940c839f4f77b0256f5edceaa04e3f32bc96a091e05846ad7ae4ef7041e7ea37a7bda91d66640b3b0d7afa14e3c62395f3f9afde40a661fc67bd8aa527d99b14907bdbccb5f32389b833d23a77e89558ed1beee4cf8cbada5851208358e1de03b5ccbc0ae21f15e0bfd7a2e295ccd12cb9949b457b1c16e17b8f93eba30cb464e4374ae6cbae72e2b47da1f912548d6414347e2627a58cb9545e63df6291d8dee16fc0b7611c3436e249f9122090274a77b5e11684f0064b782cfff2a47a18efecbc3b952368f52459b5d09d550a0416ea907ab4e3e94af247f75037bd8d729ce9b14114830cc2af8a47b11437e62ca8ad0121d5e6d9d0cf83b399252a233f616b589c1714419fc601a84c64c16e2a952e89b6c514d4d10a727f6f0e8fb9d171bddf85559905442de0f8d4f3e27e82ff96f4c9c363ce2f06bf79d0c220f747",
- "markIn": 211560,
- "markOut": 350280
- },
- {
- "id": "6820714",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820714",
- "title": "FIFA: la justice américaine estime que la corruption règne depuis des années",
- "description": "Certains accusés se seraient livrés au FBI, révélant alors les diverses fraudes soupçonnées.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820713.image/16x9",
- "imageTitle": "FIFA: la justice américaine estime que la corruption règne depuis des années [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 136960,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 3,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: la justice américaine estime que la corruption règne depuis des années",
- "media_type": "Video",
- "media_segment_id": "6820714",
- "media_episode_length": "1898",
- "media_segment_length": "137",
- "media_number_of_segment_selected": "3",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820714"
- },
- "eventData": "$744ed4db0b4cd83c$28da18646dea2bbefc7028f4ad6e3e46472924dda8f00a0558346509fc7190ea5052cf2f74ea34b012634feea17c18169124103dbc5f22b1fc628d29a2a4ca6efdce18efe7ab563adaa8e0c3d52ce1ecdf0ed915a5480421cabc2d2038f412a47b8959f782b704165559c191fce533101e6ef706e8817b07b8bbb35796e836a0a4da3699d669e915801ea879ec04b47eb27a6f7dbb918fecf3f3f53f45370396650fcac161d451a0ec129357b42f704d4dd88f7612e2294cda85621e1980b63e3e1b72b58b5f2336a8b5640738f949261859799bd44080689761fcd62d785aa3fdf4e17bdbc6cf238e66cff9033ae686f1adff97d7c7fe840b8a3876bc3101b0f07017ca15a1efaf8b59ce0d3b5eab93cbd737c9f556615a29acf17ff251c9505f28c0ea504fe827ff01036100066aebb5022665ff261b9bf5899be548889d1fcadcdc1f810e62b040729dedf2a74748837c3f7ef7a07579a6ba3e5c6c1a898c1260eac4de000ea867bf017c86c8dd42",
- "markIn": 350320,
- "markOut": 487280
- },
- {
- "id": "6820726",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820726",
- "title": "FIFA: Sepp Blatter règne seul depuis 17 ans",
- "description": "Selon certains, le Suisse a su gagner la fidélité de beaucoup de fédérations de football, grâce auxquelles il a toujours gagné les élections.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820725.image/16x9",
- "imageTitle": "FIFA: Sepp Blatter règne seul depuis 17 ans [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 136200,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 4,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: Sepp Blatter règne seul depuis 17 ans",
- "media_type": "Video",
- "media_segment_id": "6820726",
- "media_episode_length": "1898",
- "media_segment_length": "136",
- "media_number_of_segment_selected": "4",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820726"
- },
- "eventData": "$d355e2b7031e11af$3c6c3c6585841c69ca49c5f51f98b04bdf8e711be2fb5b72f13ba0efc9249a41d587548c64243e1ae12cde8a03a89cd7a492be7e1601be88f253bdfb1d424cf231f795880901197359ffd61917655814135abf809bc5bf46a35f51fa1561c0fd8eebed8497001a881365cbcd026258f2a20a3166551db8037c8876879605a21a9d19634a6d59b28a3663170bb9102c78d15746aee48c6b50776ba7157f654bdff3f53f95ddaff83a4eeb703cf0f202f4e4d5dad521ac93a04d6d92da8d623ad12cb6cb6059ff5ff540a381e57cd06ab47a4dcf84ddf8a9fb2b35f6be88ad6fdf7987b3cd135206908102e8905d2695939ce3d3b73ff9daa4a917fedbf17bdbc89fc42195042c202ae26731310d297f1b1ce91c46549eee3c316909474280d571e97484d71439cb55307fce45481cc6b383fe7e1a8cdc8bd09ed8284ba4cad8bd767605db1bcde959387ac2d0a29860af68f68ec33552ba5f4c9f78ce3fb525bc15a21061d3717e44b8e355a0c57e7a2e",
- "markIn": 550720,
- "markOut": 686920
- },
- {
- "id": "6820728",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820728",
- "title": "FIFA: l'image de la Suisse pourrait être ternie",
- "description": "Le pays a peut être été trop clément en matière d'exonération fiscale ou de lutte anti-corruption.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820727.image/16x9",
- "imageTitle": "FIFA: l'image de la Suisse pourrait être ternie [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 117000,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 5,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: l'image de la Suisse pourrait être ternie",
- "media_type": "Video",
- "media_segment_id": "6820728",
- "media_episode_length": "1898",
- "media_segment_length": "117",
- "media_number_of_segment_selected": "5",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820728"
- },
- "eventData": "$60a4f9c29b9364a6$f129372d1d920bf271d9a019a5539747c7fc6c1b34b631d428d0ca6504a633cda9b2e7c0506d3908ecbb67b3db817cc8d1824a4198ee9798242dab751a9334f8bf933535bb658d80712b08bbd1eb802c76cac516721191d4d536e369ac91dce8bdf89f213b7e688f7d34b3416a380df3511c364805316cd30f011490f0e9a7e885c04bd477a80d1991562e5ed9b23316cf75a442282d79f3913b209b047053272b43cf3e7e800082e4a67ec3dafc7220acf81163c1d3c6c16a9993e36f92ba3f6c1605e138ee7109adb98a7272f434be1f28b5df56384998848b7a90cfe0e8e8f9a12fef48bd1bc34d12bac33e03a1bf838151e6257476945ebaa706dba3eeca5d3ebfcef15142b563d4091d66f4e5bdd6ef9fb72a15b15cd3199eb752622c4e02441664825cab08ba72fd3f05101919832f8fdd493890a9c4b10ddd2b5e28448537ebb70e33df65cdcbeca997597e5e3abc40a9ebe2be5510b2c0cea2f7ae6ce2ec0e7f074eb75bae0a44ab8ad4a0d7",
- "markIn": 709480,
- "markOut": 826480
- },
- {
- "id": "6820734",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820734",
- "title": "FIFA: le point avec Pierre-Alain Dupuis, à Zurich",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820733.image/16x9",
- "imageTitle": "FIFA: le point avec Pierre-Alain Dupuis, à Zurich [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 236720,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 6,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "FIFA: le point avec Pierre-Alain Dupuis, à Zurich",
- "media_type": "Video",
- "media_segment_id": "6820734",
- "media_episode_length": "1898",
- "media_segment_length": "237",
- "media_number_of_segment_selected": "6",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820734"
- },
- "eventData": "$1539564dda06e4ca$ee4960ade603b9e2b75f143d82232eff1e9863e26dfc18c33b15ff6901b136068da389ab2c99ef755b15f659ece03ee53df4075829c5f3443998711c251245fd59af3a1dcf3323fe40d7bb78c83ad6a6cac42daf2f6fff98affd7a016ced32c09f752cdb5114e34d28091395d54b2ccd0aca5aa79e395b06e31f498271f3e24ac8ec0d0db2d58404e0517e1f4580a511375a9bac52777b480624a2e0750e9c1aad5b433ada350b68fc27f4f6e29d0b15b09bbe02d41bdc263ac7f832d125be2e1fa093de75290258fa605c3d4ab3f708a79dd0c3f145c265b4e1233efaf0d5e8e1cfe5c5a1e4bdb161b6026d7881f509844ad4b9f4c9673c8a22c0d2a3eb51b1c82d057a5502a6eb6843674506fc0238001968dfd2db8ea2713bfa9eb1062e2270f26982088460d85df916a3c1b6070c236e4063465710f378a7ec7c42173b7b634e4eb00f17f730769ca79bcd7fe604b86364edbbb718a1d49b39d27f928a617012878bed166315069b914cd6b43b87",
- "markIn": 826520,
- "markOut": 1063240
- },
- {
- "id": "6820718",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820718",
- "title": "Votations: le diagnostic préimplantatoire est décrié par Insieme",
- "description": "L'association craint que le DPI dissuade davantage de parents de donner naissance à un enfant handicapé.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820717.image/16x9",
- "imageTitle": "Votations: le diagnostic préimplantatoire est décrié par Insieme [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 123480,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 7,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Votations: le diagnostic préimplantatoire est décrié par Insieme",
- "media_type": "Video",
- "media_segment_id": "6820718",
- "media_episode_length": "1898",
- "media_segment_length": "123",
- "media_number_of_segment_selected": "7",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820718"
- },
- "eventData": "$ade26625e0fe6525$60f00fe8d073893fb0cbfc03fd419d0100e81dd197ecc2efb6c2380424b1e28c66bec056cedc009dbe96e9e23c2846b22de4151f70eb52f2e720b9a2f41fbbf20a6ed49ba9bb714458c644affe662740812b6262180981a5d613629e8152b6ffaa90c45a70af69d13312574da70557f454f5acd9aa44ec7521041239bcc936c0f790fc4cea8cf59ccf780a35817698dc91fb11f4f956740fcb71fe1a44faaaa5156f296f7acb4656c19ccd6a1a521834f0d79cc73b089f43cd59b3decf644590653f06e01737a01eebf33818322173093bcffef561cc5e966f4a40fb71bb32741c0b239f7ff5f96f0e6de61eb29ad5a77ef792d0ab497c31b30af429b529395a74811003a5209852e7cf8719d7041d554c0ad3355280f0348a5d1653fc254c3ed81b18ae757bc6bc7a56ea0743585e69ba747fbc11f1700c8032c78943ff71dc2e47a7fa7567f0120b69a288917c6af0968e9274ede26bd71e16ae1550182c7851370932892af87c76e053ad46f23e65",
- "markIn": 1063520,
- "markOut": 1187000
- },
- {
- "id": "6820724",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820724",
- "title": "Votations / DPI: les explications d'Amélie Boguet",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820723.image/16x9",
- "imageTitle": "Votations - DPI: les explications d'Amélie Boguet [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 81000,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 8,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Votations / DPI: les explications d'Amélie Boguet",
- "media_type": "Video",
- "media_segment_id": "6820724",
- "media_episode_length": "1898",
- "media_segment_length": "81",
- "media_number_of_segment_selected": "8",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820724"
- },
- "eventData": "$a1943a612153bfc3$4a806744914b9a5ee3cd4ec1ab701ef362fb2d1f257ebbd7c93627f3304f6e7cfc882a9db30abecd1da7f10fdf1dd1bc6d0a92d28da7be5a4bd7f842c616873ce280952e44af59fc19f2cf3d9b09dcdfa460b00eb6cf8bd86b72939f9edbfd2f347ac94b53d68a480b98f6fc94434aa45a1d9cbb34b22306d983e486b5f551dfaedf0e761ca7d052c475c3541d49aa5eed76eb040d1223e63d9a472954b084b7aa32ebb086d3ec5980875bad845204b02beac844f4c2230ce0c661f3ea036466374300aef4433cf29cba23611d0229c217d131c6714afa8647b7ee4157b300a4fc090fdef0c623c58fac5a7c995498cb231d772d2aa157e8f03ad709df4e1f8eba05fc1732a4c3a306c88fd96a50652f54ccb13eff83a61dc52486a9aecb93288437a31bd59cad27e16b28c990b1d5cfeb377fcc8446195acc73481f665c69cf8c7fdbb4dd43a11cfcf3f8e2c66a7a4b75e2c9852cbd7c94f96cb4f95b3b650fbc5697615fb7d50d20e1dbda9ebac91c",
- "markIn": 1187320,
- "markOut": 1268320
- },
- {
- "id": "6820732",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820732",
- "title": "Votations / DPI: l'interdiction du diagnostic nuit à certains couples",
- "description": "Certains parents ou futurs parents voyagent alors pour consulter, ce qui n'est pas toujours évident selon les ressources du couple.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820731.image/16x9",
- "imageTitle": "Votations - DPI: l'interdiction du diagnostic nuit à certains couples [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 175800,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 9,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Votations / DPI: l'interdiction du diagnostic nuit à certains couples",
- "media_type": "Video",
- "media_segment_id": "6820732",
- "media_episode_length": "1898",
- "media_segment_length": "176",
- "media_number_of_segment_selected": "9",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820732"
- },
- "eventData": "$cba09d3c27881ad8$be2815de766b8df541dac5848a814e4a6cf12c597331f8d22964c8d16af6cf956d9d4e7ba103b7d26072d90315421bc56a02f67d5fe16d2261fd182e0da59c0c84291b0c3380a497f838796977eab366f218fb54871875beea9eeaf8332a0ca1439bc7960b3caac1efabab403a06cf6d77c3a98f4ff317eb1bb70d55d848331578a59865ce68c9eacc8411243bbb8abe42c0c49ba5b682d8e3ce38753b01f7d8da2290d1ccc104a7cde523cbef657786c1225a62f8ba4b7e8b8d6392e487b4d31d8e232f62adcf0802153fe264dbfb610391cac494d5a88a0703cc49e3a1a52267973f9360ddee81bb9e718b82d6df5ffee9f336e22fe9d6116d36ed55867ea7f10f508ab93cf88804d805b8be3074943d2fa80d596ed88fa9bc8971ba8d9283c071f07c9a1c7720bffd22602474692c6cdbd0e23942e105a4e8c1ead3643ebe3a5a8627587f613fc9e71ded543c471c6271f659eec59974c35c27a0af634ef8993463ad9e9b698a1c1943f8f979b6d2",
- "markIn": 1268360,
- "markOut": 1444160
- },
- {
- "id": "6820722",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820722",
- "title": "GE: un géant chinois de la pharma, Tasly, s'installe",
- "description": "Plusieurs dizaines d'emplois vont être créés et une usine pourrait alors voir le jour dans le canton de Fribourg.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820721.image/16x9",
- "imageTitle": "GE: un géant chinois de la pharma, Tasly, s'installe [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 125040,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 10,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "GE: un géant chinois de la pharma, Tasly, s'installe",
- "media_type": "Video",
- "media_segment_id": "6820722",
- "media_episode_length": "1898",
- "media_segment_length": "125",
- "media_number_of_segment_selected": "10",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820722"
- },
- "eventData": "$9749fae13beede7f$ab571d879779715a84afcb18c19aa9e9705f19fceeb529f87830ec4d5eb59e0a2c5e27f879d2ed70c5052f051d840f86c935ffb7867cd16ce453c5c3947d72dfd61dfba2c41c44ec3ea8083e96ecf3c5416578245d2dfdf1951f9cd10f801df43291a941ef45bc9a2559b1b27c8cfa731e9876c5eb19701656450c0f2291f29c3550e2387191b8d2a2fc10fbe2b8224b8a5a1dfa5252404bd16ba07eed855287bc6edd5939280c2e7543a9895e003606b1c55bf13d80c6130797c2d678dcf99df16c0f9905a85e5f6786a2d48adf67ad15ac21b605be2cf4c978762fff550503d3d603003172881ad96060fe4e1b916a62f754b0914aad7af54e6eb08260ea9f109cd6cbc669895930b02ea906fd06d609342d1fb51a7f2a6c961d1972b77dc5ff0dd9e11005696597f4eb614e3b13ef3235232a5b1e31bb82ba47721f6b6d1d45c5ba0b543e91dc992589b90f9e7586b9ad996dea6a5a0497516b0f1bcaf19fbeb2d9fd1ba1f0f132960e17d467eda5",
- "markIn": 1444200,
- "markOut": 1569240
- },
- {
- "id": "6820716",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820716",
- "title": "Le planning familial vient en aide depuis 50 ans",
- "description": "Les jeunes profitent de ce service qui contribue à promouvoir la qualité de vie dans les différentes étapes de la vie affective.",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820715.image/16x9",
- "imageTitle": "Le planning familial vient en aide depuis 50 ans [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 120080,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 11,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Le planning familial vient en aide depuis 50 ans",
- "media_type": "Video",
- "media_segment_id": "6820716",
- "media_episode_length": "1898",
- "media_segment_length": "120",
- "media_number_of_segment_selected": "11",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820716"
- },
- "eventData": "$11797d93867fbbff$38abb41acc9aefc030d92b0c08a30ac4dd99bfd90c5d79ee70625b0519aa4477bc180bdab31ffdbab5e4fed3fd19a19ace1d75ce945fed9b1c8293749a0c6dece33ccea0e5f3dcfb290fa9980c3dcb5e287993b0a5a21d6b74f6e1656484beb85f02765a8947c4dd2da0d43cd6749504af6cb41db825c800a66e5170d9cf0f50a0f01de3ca2e122809975571305a9ce819378902e5418dbb1c0d433d422fb1658df6fc50a1e8bcb32761a0c93fde9d133b3272e1cac063deee68b5b5e22ed9da396aea852d80bf12bec5a5f414a2ad8978dcc64cc26baa748eebb59a1eab8f68ae5dc68c9cc38d7444562727400f3d0a3261ce910630dd411d016be0a00f5c3f6a868b90cc276eef092309ce74582eeaf9117c83bf17cbea217b254f556fb8900d68188b090ee17efe70cef56c1df2c7bd43e87096db7b6f9d4e4eeab02ec8bc47bc2047cde332aaf0483eccdf43e04a99e63f58c9cc689a4505e3cefd858fe2dbe868c765bffd302e9ed9092aec85df",
- "markIn": 1569280,
- "markOut": 1689360
- },
- {
- "id": "6820730",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:6820730",
- "title": "Musique: les choeurs d'enfants restent une contribution courante",
- "description": "Comme d'autres avant lui, Raphael s'est alors entouré d'enfants pour son nouvel album \"Somnambule\".",
- "imageUrl": "https://www.rts.ch/2015/05/28/20/19/6820729.image/16x9",
- "imageTitle": "Musique: les choeurs d'enfants restent une contribution courante [RTS]",
- "type": "CLIP",
- "date": "2015-05-28T19:30:00+02:00",
- "duration": 154040,
- "validFrom": "2015-05-28T20:01:00+02:00",
- "playableAbroad": true,
- "displayable": true,
- "fullLengthUrn": "urn:rts:video:6820736",
- "position": 12,
- "noEmbed": false,
- "analyticsMetadata": {
- "media_segment": "Musique: les choeurs d'enfants restent une contribution courante",
- "media_type": "Video",
- "media_segment_id": "6820730",
- "media_episode_length": "1898",
- "media_segment_length": "154",
- "media_number_of_segment_selected": "12",
- "media_number_of_segments_total": "12",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:6820730"
- },
- "eventData": "$e1ebe74c015c0508$b2b03bca04a0e4690733fdf7f37fafef858a41cfbb4405711f2c476c80be7c65979f60c6db608134bb27c42cd3f135f43afd8e1a711595119196d12efd4234071030d272a1bd444a29a78c719fd1218127d5f2f5f6a1d8df9ce3d9844cca732d3e67bcc9dd09427d7a22da6ab3762d2a91cd95eaa0046c644005e09d1f8497ec18b768354968eeae241d0463b8c6220319518cf91e9285aecf41955054038cd5fb5ec6127eab5ba39c556f1a08684eabe561e4892d75cc3e49ff2cbde8b14d3c1df27899ab3db3fd34eb2294420652bdcf713e3763ea544ff7102d29210c9f26b0aa073cc133777d13713e693c8f7e845421ab6b4e7978c6b1d1fc2793610b17f098650a63ad2f5494cd3c42743188dbf4970fede30e50e1b0f58a6efa4cef80fa82e68eeaefc5f8791f07b3ae12dc1a9d0f9c85a8e390d2f060e263cee964333843ff8a2ce7df9f5952678be4ae1fada33a915a454987ae2119a91c3ecfbdb17492268e2e116464e2dffda472ee814f",
- "markIn": 1689400,
- "markOut": 1843440
- }
- ],
- "aspectRatio": "16:9"
- }
- ],
- "topicList": [
- {
- "id": "908",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:908",
- "title": "19h30"
- },
- {
- "id": "904",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:904",
- "title": "Vidéos"
- },
- {
- "id": "665",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:665",
- "title": "Info"
- }
- ],
- "analyticsData": {
- "srg_pr_id": "6703608",
- "srg_plid": "105932",
- "ns_st_pl": "19h30",
- "ns_st_pr": "19h30 du 28.05.2015",
- "ns_st_dt": "2015-05-28",
- "ns_st_ddt": "2015-05-28",
- "ns_st_tdt": "2015-05-28",
- "ns_st_tm": "19:30:00",
- "ns_st_tep": "f_858979",
- "ns_st_li": "0",
- "ns_st_stc": "0867",
- "ns_st_st": "RTS Online",
- "ns_st_tpr": "105932",
- "ns_st_en": "*null",
- "ns_st_ge": "*null",
- "ns_st_ia": "*null",
- "ns_st_ce": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc",
- "srg_unit": "RTS",
- "srg_c1": "full",
- "srg_c2": "video_info_journal-19h30",
- "srg_c3": "RTS 1",
- "srg_tv_id": "f_858979"
- },
- "analyticsMetadata": {
- "media_episode_id": "6703608",
- "media_show_id": "105932",
- "media_show": "19h30",
- "media_episode": "19h30 du 28.05.2015",
- "media_is_livestream": "false",
- "media_full_length": "full",
- "media_enterprise_units": "RTS",
- "media_joker1": "full",
- "media_joker2": "video_info_journal-19h30",
- "media_joker3": "RTS 1",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_tv_id": "f_858979",
- "media_thumbnail": "https://www.rts.ch/2015/05/28/20/19/6820735.image/16x9/scale/width/344",
- "media_publication_date": "2015-05-28",
- "media_publication_time": "20:01:00",
- "media_publication_datetime": "2015-05-28T20:01:00+02:00",
- "media_tv_date": "2015-05-28",
- "media_tv_time": "19:30:00",
- "media_tv_datetime": "2015-05-28T19:30:00+02:00",
- "media_content_group": "19h30,Vidéos,Info",
- "media_channel_id": "143932a79bb5a123a646b68b1d1188d7ae493e5b",
- "media_channel_cs": "0867",
- "media_channel_name": "RTS 1",
- "media_since_publication_d": "2905",
- "media_since_publication_h": "69733"
- }
- },
- {
- "chapterUrn": "urn:rts:video:8841634",
- "episode": {
- "id": "8741989",
- "title": "Couleur 3 en direct",
- "publishedDate": "2017-01-14T15:08:55+01:00",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3"
- },
- "show": {
- "id": "8483936",
- "vendor": "RTS",
- "transmission": "RADIO",
- "urn": "urn:rts:show:radio:8483936",
- "title": "Couleur 3 en vidéos",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "bannerImageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/3x1",
- "posterImageUrl": "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg",
- "posterImageIsFallbackUrl": true,
- "primaryChannelId": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "primaryChannelUrn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "audioDescriptionAvailable": false,
- "subtitlesAvailable": false,
- "multiAudioLanguagesAvailable": false,
- "topicList": [
- {
- "id": "16208",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:16208",
- "title": "Couleur 3"
- }
- ],
- "allowIndexing": false
- },
- "channel": {
- "id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "vendor": "RTS",
- "urn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "title": "Couleur 3",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "transmission": "RADIO"
- },
- "chapterList": [
- {
- "id": "8841634",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:8841634",
- "title": "Couleur 3 en direct",
- "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
- "imageTitle": "Chaîne Couleur 3",
- "type": "LIVESTREAM",
- "date": "2017-01-14T15:08:55+01:00",
- "duration": 0,
- "playableAbroad": true,
- "displayable": true,
- "position": 0,
- "noEmbed": false,
- "analyticsData": {
- "ns_st_ep": "Livestream",
- "ns_st_ty": "Video",
- "ns_st_ci": "8841634",
- "ns_st_el": "0",
- "ns_st_cl": "0",
- "ns_st_sl": "0",
- "srg_mgeobl": "false",
- "ns_st_tp": "1",
- "ns_st_cn": "1",
- "ns_st_ct": "vc13",
- "ns_st_pn": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc"
- },
- "analyticsMetadata": {
- "media_segment": "Livestream",
- "media_type": "Video",
- "media_segment_id": "8841634",
- "media_episode_length": "0",
- "media_segment_length": "0",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "1",
- "media_duration_category": "infinit.livestream",
- "media_is_geoblocked": "false",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_urn": "urn:rts:video:8841634"
- },
- "fullLengthMarkIn": 0,
- "fullLengthMarkOut": 0,
- "resourceList": [
- {
- "url": "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0",
- "quality": "HD",
- "protocol": "HLS",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": false,
- "live": true,
- "mediaContainer": "MPEG2_TS",
- "audioCodec": "AAC",
- "videoCodec": "H264",
- "tokenType": "NONE",
- "analyticsData": {
- "srg_mqual": "HD",
- "srg_mpres": "DEFAULT"
- },
- "analyticsMetadata": {
- "media_streaming_quality": "HD",
- "media_special_format": "DEFAULT",
- "media_url": "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0"
- }
- }
- ],
- "aspectRatio": "16:9"
- }
- ],
- "topicList": [
- {
- "id": "16208",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:16208",
- "title": "Couleur 3"
- }
- ],
- "analyticsData": {
- "srg_pr_id": "8741989",
- "srg_plid": "8483936",
- "ns_st_pl": "Livestream",
- "ns_st_pr": "Couleur 3 en direct",
- "ns_st_dt": "2017-01-14",
- "ns_st_ddt": "2017-01-14",
- "ns_st_tdt": "2017-01-14",
- "ns_st_tm": "15:08:55",
- "ns_st_tep": "*null",
- "ns_st_li": "1",
- "ns_st_stc": "0867",
- "ns_st_st": "Couleur 3",
- "ns_st_tpr": "11562086",
- "ns_st_en": "*null",
- "ns_st_ge": "*null",
- "ns_st_ia": "*null",
- "ns_st_ce": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc",
- "srg_unit": "RTS",
- "srg_c1": "live",
- "srg_c2": "rts.ch_video_couleur3",
- "srg_c3": "COULEUR 3",
- "srg_tv_id": "3f1e4c4e-0f1e-479b-92b7-16a9f064a2e3"
- },
- "analyticsMetadata": {
- "media_episode_id": "8741989",
- "media_show_id": "11562086",
- "media_show": "Oui Mais Non",
- "media_episode": "Couleur 3 en direct",
- "media_is_livestream": "true",
- "media_full_length": "full",
- "media_enterprise_units": "RTS",
- "media_joker1": "live",
- "media_joker2": "rts.ch_video_couleur3",
- "media_joker3": "COULEUR 3",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_thumbnail": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9/scale/width/344",
- "media_publication_date": "2017-01-14",
- "media_publication_time": "15:08:55",
- "media_publication_datetime": "2017-01-14T15:08:55+01:00",
- "media_tv_date": "2017-01-14",
- "media_tv_time": "15:08:55",
- "media_tv_datetime": "2017-01-14T15:08:55+01:00",
- "media_content_group": "Couleur 3",
- "media_channel_id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
- "media_channel_cs": "0867",
- "media_channel_name": "Couleur 3",
- "media_since_publication_d": "2308",
- "media_since_publication_h": "55409"
- }
- },
- {
- "chapterUrn": "urn:rts:video:13444428",
- "episode": {
- "id": "13444410",
- "title": "RENCA",
- "publishedDate": "2022-10-06T16:58:00+02:00",
- "imageUrl": "https://www.rts.ch/2022/10/06/17/32/13444418.image/4x5",
- "imageTitle": "08 Outro [RTS]"
- },
- "show": {
- "id": "12698364",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:show:tv:12698364",
- "title": "Le rencard",
- "imageUrl": "https://ws.srf.ch/asset/image/audio/8b32fd87-459e-40f1-9519-ba0cbb01f34e/NOT_SPECIFIED.jpg",
- "posterImageUrl": "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg",
- "posterImageIsFallbackUrl": true,
- "audioDescriptionAvailable": false,
- "subtitlesAvailable": false,
- "multiAudioLanguagesAvailable": false,
- "topicList": [
- {
- "id": "68210",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:68210",
- "title": "Le rencard"
- },
- {
- "id": "2451",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:2451",
- "title": "Émissions"
- }
- ],
- "allowIndexing": false
- },
- "chapterList": [
- {
- "id": "13444428",
- "mediaType": "VIDEO",
- "vendor": "RTS",
- "urn": "urn:rts:video:13444428",
- "title": "08 Outro",
- "imageUrl": "https://www.rts.ch/2022/10/06/17/32/13444418.image/4x5",
- "imageTitle": "08 Outro [RTS]",
- "type": "EPISODE",
- "date": "2022-10-06T16:58:00+02:00",
- "duration": 7320,
- "validFrom": "2022-10-06T16:58:00+02:00",
- "playableAbroad": true,
- "socialCountList": [
- {
- "key": "srgView",
- "value": 278
- },
- {
- "key": "srgLike",
- "value": 0
- },
- {
- "key": "fbShare",
- "value": 0
- },
- {
- "key": "twitterShare",
- "value": 0
- },
- {
- "key": "googleShare",
- "value": 0
- },
- {
- "key": "whatsAppShare",
- "value": 0
- }
- ],
- "displayable": true,
- "position": 0,
- "noEmbed": false,
- "analyticsData": {
- "ns_st_ep": "08 Outro",
- "ns_st_ty": "Video",
- "ns_st_ci": "13444428",
- "ns_st_el": "7320",
- "ns_st_cl": "7320",
- "ns_st_sl": "7320",
- "srg_mgeobl": "false",
- "ns_st_tp": "1",
- "ns_st_cn": "1",
- "ns_st_ct": "vc11",
- "ns_st_pn": "1",
- "ns_st_cdm": "eo",
- "ns_st_cmt": "ec"
- },
- "analyticsMetadata": {
- "media_segment": "08 Outro",
- "media_type": "Video",
- "media_segment_id": "13444428",
- "media_episode_length": "7",
- "media_segment_length": "7",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "1",
- "media_duration_category": "short",
- "media_is_geoblocked": "false",
- "media_is_web_only": "true",
- "media_production_source": "produced.for.web",
- "media_urn": "urn:rts:video:13444428"
- },
- "eventData": "$31560b9cf8eff8a6$53ec475eaefd11e175a15a342b6a0fc3c0007f3bac816d49f9fe53f94ba8363f5977b4f21de60406f24d3f00178694c1b642c984456c298a2d2ac4c330639eb3a0fc1e2db2171ce26fb28ecb6621b17577681bff6de80e73a88a5b73f0ede629c2f529d4b09d9981979fa9ed112a82b06c4c86f2cc8a265b127932b8e9ff0da13a34d6e1199004cf954b20c65670330a9f158493934be068d96c20622212189c09f16d251bd73a6defdac626d899e06d874f00fe6ec1e36ab100a7fc9add7e67c82472868ff61f24091ccb4b4ab7489601b61e2505456c5a51b1f1ec0ebe468b3c87cfecee2a3f0d28dad0f46a1f7b2ec40d5d39dbfffea6950b1bf6971aabfd71fc4dcc39724e972aa2b8f062c27e89d58aaf69830b98b6eb22417e554b7c730dff83367f97b99153afe7c2c6e230cfb8cf5eb62b807c6d352966eaadac5aab",
- "resourceList": [
- {
- "url": "https://rts-vod-amd.akamaized.net/ww/13444428/857d97ef-0b8e-306e-bf79-3b13e8c901e4/master.m3u8",
- "quality": "HD",
- "protocol": "HLS",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": false,
- "live": false,
- "mediaContainer": "FMP4",
- "audioCodec": "AAC",
- "videoCodec": "H264",
- "tokenType": "NONE",
- "audioTrackList": [
- {
- "locale": "fr",
- "language": "Français",
- "source": "HLS"
- }
- ],
- "analyticsData": {
- "srg_mqual": "HD",
- "srg_mpres": "DEFAULT"
- },
- "analyticsMetadata": {
- "media_streaming_quality": "HD",
- "media_special_format": "DEFAULT",
- "media_url": "https://rts-vod-amd.akamaized.net/ww/13444428/857d97ef-0b8e-306e-bf79-3b13e8c901e4/master.m3u8"
- }
- }
- ],
- "aspectRatio": "9:16"
- }
- ],
- "topicList": [
- {
- "id": "68210",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:68210",
- "title": "Le rencard"
- },
- {
- "id": "2451",
- "vendor": "RTS",
- "transmission": "TV",
- "urn": "urn:rts:topic:tv:2451",
- "title": "Émissions"
- }
- ],
- "analyticsData": {
- "srg_pr_id": "13444410",
- "srg_plid": "12698364",
- "ns_st_pl": "Le rencard",
- "ns_st_pr": "Le rencard du 06.10.2022",
- "ns_st_dt": "2022-10-06",
- "ns_st_ddt": "2022-10-06",
- "ns_st_tdt": "*null",
- "ns_st_tm": "*null",
- "ns_st_tep": "253058342-43898",
- "ns_st_li": "0",
- "ns_st_stc": "0867",
- "ns_st_st": "RTS Online",
- "ns_st_tpr": "12698364",
- "ns_st_en": "*null",
- "ns_st_ge": "*null",
- "ns_st_ia": "*null",
- "ns_st_ce": "1",
- "ns_st_cdm": "eo",
- "ns_st_cmt": "ec",
- "srg_unit": "RTS",
- "srg_c1": "full",
- "srg_c2": "video_emissions_le-rencard",
- "srg_c3": "RTS.ch",
- "srg_tv_id": "253058342-43898"
- },
- "analyticsMetadata": {
- "media_episode_id": "13444410",
- "media_show_id": "12698364",
- "media_show": "Le rencard",
- "media_episode": "Le rencard du 06.10.2022",
- "media_is_livestream": "false",
- "media_full_length": "full",
- "media_enterprise_units": "RTS",
- "media_joker1": "full",
- "media_joker2": "video_emissions_le-rencard",
- "media_joker3": "RTS.ch",
- "media_is_web_only": "true",
- "media_production_source": "produced.for.web",
- "media_tv_id": "253058342-43898",
- "media_thumbnail": "https://www.rts.ch/2022/10/06/17/32/13444418.image/4x5/scale/width/344",
- "media_publication_date": "2022-10-06",
- "media_publication_time": "16:58:00",
- "media_publication_datetime": "2022-10-06T16:58:00+02:00",
- "media_content_group": "Le rencard,Émissions",
- "media_since_publication_d": "216",
- "media_since_publication_h": "5207"
- }
- },
- {
- "chapterUrn": "urn:srf:video:f10ba470-6a3c-4479-8b2a-4529f7066234",
- "episode": {
- "id": "dcc5b238-7943-4fe7-8636-e41989d91408",
- "title": "Tagesschau vom 09.05.2023: Mittagsausgabe",
- "publishedDate": "2023-05-09T12:45:00+02:00",
- "imageUrl": "https://ws.srf.ch/asset/image/audio/7e18e733-622b-4bd5-9323-971239e49844/WEBVISUAL/1607950376.jpg"
- },
- "show": {
- "id": "ff969c14-c5a7-44ab-ab72-14d4c9e427a9",
- "vendor": "SRF",
- "transmission": "TV",
- "urn": "urn:srf:show:tv:ff969c14-c5a7-44ab-ab72-14d4c9e427a9",
- "title": "Tagesschau",
- "lead": "Nationale und internationale Nachrichten vom Tag.",
- "description": "Die «Tagesschau» berichtet über Themen aus Politik, Wirtschaft, Kultur, Sport, Gesellschaft und Wissenschaft aus dem In- und Ausland. ",
- "imageUrl": "https://ws.srf.ch/asset/image/audio/7e18e733-622b-4bd5-9323-971239e49844/WEBVISUAL/1607950376.jpg",
- "imageTitle": "Tagesschau",
- "posterImageUrl": "https://ws.srf.ch/asset/image/audio/7e18e733-622b-4bd5-9323-971239e49844/POSTER/1681820057.jpg",
- "posterImageIsFallbackUrl": false,
- "timeTableUrl": "https://www.srf.ch/programm/tv/mediagroup/ts20",
- "links": [
- {
- "title": "In Gebärdensprache",
- "link": "https://www.srf.ch/play/tv/sendung/tagesschau-in-gebaerdensprache?id=c40bed81-b150-0001-2b5a-1e90e100c1c0"
- },
- {
- "title": "Tagesschau Spezial",
- "link": "https://www.srf.ch/play/tv/sendung/tagesschau-spezial?id=c4b213cd-9790-0001-3063-e4001037ebe0"
- },
- {
- "title": "SRF Augenzeuge",
- "link": "https://www.srf.ch/meteo/uebersicht/zuschauer-bilder-srf-augenzeuge"
- }
- ],
- "primaryChannelId": "23FFBE1B-65CE-4188-ADD2-C724186C2C9F",
- "primaryChannelUrn": "urn:srf:channel:tv:23FFBE1B-65CE-4188-ADD2-C724186C2C9F",
- "numberOfEpisodes": 17567,
- "topicList": [
- {
- "id": "a709c610-b275-4c0c-a496-cba304c36712",
- "vendor": "SRF",
- "transmission": "TV",
- "urn": "urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712",
- "title": "News",
- "lead": "Hier finden Sie alle Sendungen von SRF zum Themenbereich News: aktuell, informativ und tiefgründig.",
- "description": "Tagesschau, Meteo, 10vor10, Schweiz aktuell, Börse, Kinder News, Forward - die News-Sendungen von SRF stehen für seriös recherchierte Berichterstattung und kompetente Hintergrundberichterstattung."
- }
- ]
- },
- "channel": {
- "id": "23FFBE1B-65CE-4188-ADD2-C724186C2C9F",
- "vendor": "SRF",
- "urn": "urn:srf:channel:tv:23FFBE1B-65CE-4188-ADD2-C724186C2C9F",
- "title": "SRF 1",
- "imageUrl": "https://ws.srf.ch/asset/image/audio/d91bbe14-55dd-458c-bc88-963462972687/EPISODE_IMAGE",
- "imageUrlRaw": "https://il.srgssr.ch/image-service/dynamic/536ef7.svg",
- "imageTitle": "Logo",
- "transmission": "TV"
- },
- "chapterList": [
- {
- "id": "f10ba470-6a3c-4479-8b2a-4529f7066234",
- "mediaType": "VIDEO",
- "vendor": "SRF",
- "urn": "urn:srf:video:f10ba470-6a3c-4479-8b2a-4529f7066234",
- "title": "Tagesschau vom 09.05.2023: Mittagsausgabe",
- "lead": "CS bleibt vorerst eine eigenständige Bank, Pensionskassen machen 100 Milliarden Franken Verlust, Russland feiert Tag des Sieges in abgespeckter Form",
- "imageUrl": "https://ws.srf.ch/asset/image/audio/c7f097f3-2738-4bc1-ae41-244399a3fbc9/EPISODE_IMAGE/1683630053.png",
- "imageTitle": "Tagesschau vom 09.05.2023: Mittagsausgabe",
- "type": "EPISODE",
- "date": "2023-05-09T12:45:00+02:00",
- "duration": 712800,
- "validFrom": "2023-05-09T12:45:00+02:00",
- "playableAbroad": true,
- "socialCountList": [
- {
- "key": "srgView",
- "value": 10856
- },
- {
- "key": "srgLike",
- "value": 0
- },
- {
- "key": "fbShare",
- "value": 1
- },
- {
- "key": "twitterShare",
- "value": 0
- },
- {
- "key": "googleShare",
- "value": 0
- },
- {
- "key": "whatsAppShare",
- "value": 4
- }
- ],
- "displayable": true,
- "position": 0,
- "noEmbed": false,
- "analyticsData": {
- "ns_st_ep": "Tagesschau vom 09.05.2023: Mittagsausgabe",
- "ns_st_ty": "Video",
- "ns_st_ci": "f10ba470-6a3c-4479-8b2a-4529f7066234",
- "ns_st_el": "712800",
- "ns_st_cl": "712800",
- "ns_st_sl": "712800",
- "srg_mgeobl": "false",
- "ns_st_tp": "1",
- "ns_st_cn": "1",
- "ns_st_ct": "vc12",
- "ns_st_pn": "1"
- },
- "analyticsMetadata": {
- "media_segment": "Tagesschau vom 09.05.2023: Mittagsausgabe",
- "media_type": "Video",
- "media_segment_id": "f10ba470-6a3c-4479-8b2a-4529f7066234",
- "media_episode_length": "713",
- "media_segment_length": "713",
- "media_number_of_segment_selected": "1",
- "media_number_of_segments_total": "1",
- "media_duration_category": "long",
- "media_is_geoblocked": "false",
- "media_urn": "urn:srf:video:f10ba470-6a3c-4479-8b2a-4529f7066234",
- "media_assigned_tags": "srfnews"
- },
- "eventData": "$6ff2a48d0d462b41$410ff54708fa967b1b5a4863b98b70e7c08f8b223eb4fbd85239404f0c7d12e0a8502795f1a5462f38175ce213be74ba77a88aeb3b50a06da75cb87cdaaae2ecb4f04ee334d9f58f52d9cf4ee06b3da9c27148a35023b64a6e15ddfeddc204095a92129c1f367abce039ec22cfcae78362dfb3c73cba811f9a848fe636c5f970d4e3bfed6e091a49b2ef83719cd19b6cba56641f4545d007aaa5b472d769cffbb13395244e9de20637f11669d1ffea314fd65ff529adcaade576174acf1e5c714753b38aa9cb9ed7644492362f1e012127cfe713d0ab37bf8a44a1972e32ed44e3cea634ceffaac2dd32e80fe0ee275e3554acfa4c22898d383da9e10cb44fb5ee8ecafeb085f8fa4a97b5596f8f550934e59d95bf39bc3a6442f32435052210fc68a4d4fc0fc3bf0496d5eccf05c4394458ef67312664cf9db40cda80996b8391e75620d1b905fa1436d6182f410302d4858560ac520ab7f96015aa23da7586f8eb1d7777315a86dcf2000ddc1ec5e2280716c91f7a4d8753fb6cfaf30519c20c319f945a59a76207ac17c45e67c1b424dec0abd7bd3a9d584d91c4147b7e39bdf9e7a303edc4b088f72e71043f283047d65a4d6d90e6c4b2d89b1db76d5ea7f28bf20108ff99d67e8b57884bf39d811adc0a22182fbfce9044bec9956d687903c332aef6baf7ae221d85a077a86870f524692593169e7d71abbc19f5647a2c",
- "tagList": [
- "srfnews"
- ],
- "resourceList": [
- {
- "url": "https://srf-vod-amd.akamaized.net/world/hls/ts20/2023/05/ts20_20230509_124500_18781731_v_webcast_h264_,q40,q10,q20,q30,q50,.mp4.csmil/master.m3u8?caption=srf/dcc5b238-7943-4fe7-8636-e41989d91408/episode/de/vod/vod.m3u8:de:Deutsch:sdh&webvttbaseurl=subtitles.eai-general.aws.srf.ch",
- "quality": "SD",
- "protocol": "HLS",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": false,
- "live": false,
- "mediaContainer": "MP4",
- "audioCodec": "UNKNOWN",
- "videoCodec": "H264",
- "tokenType": "NONE",
- "subtitleInformationList": [
- {
- "locale": "de",
- "language": "Deutsch",
- "source": "HLS",
- "type": "SDH"
- }
- ],
- "analyticsData": {
- "srg_mqual": "SD",
- "srg_mpres": "DEFAULT"
- },
- "analyticsMetadata": {
- "media_streaming_quality": "SD",
- "media_special_format": "DEFAULT",
- "media_url": "https://srf-vod-amd.akamaized.net/world/hls/ts20/2023/05/ts20_20230509_124500_18781731_v_webcast_h264_,q40,q10,q20,q30,q50,.mp4.csmil/master.m3u8?caption=srf/dcc5b238-7943-4fe7-8636-e41989d91408/episode/de/vod/vod.m3u8:de:Deutsch:sdh&webvttbaseurl=subtitles.eai-general.aws.srf.ch"
- }
- },
- {
- "url": "https://srf-vod-amd.akamaized.net/world/hls/ts20/2023/05/ts20_20230509_124500_18781731_v_webcast_h264_,q40,q10,q20,q30,q50,q60,.mp4.csmil/master.m3u8?caption=srf/dcc5b238-7943-4fe7-8636-e41989d91408/episode/de/vod/vod.m3u8:de:Deutsch:sdh&webvttbaseurl=subtitles.eai-general.aws.srf.ch",
- "quality": "HD",
- "protocol": "HLS",
- "encoding": "H264",
- "mimeType": "application/x-mpegURL",
- "presentation": "DEFAULT",
- "streaming": "HLS",
- "dvr": false,
- "live": false,
- "mediaContainer": "MP4",
- "audioCodec": "UNKNOWN",
- "videoCodec": "H264",
- "tokenType": "NONE",
- "subtitleInformationList": [
- {
- "locale": "de",
- "language": "Deutsch",
- "source": "HLS",
- "type": "SDH"
- }
- ],
- "analyticsData": {
- "srg_mqual": "HD",
- "srg_mpres": "DEFAULT"
- },
- "analyticsMetadata": {
- "media_streaming_quality": "HD",
- "media_special_format": "DEFAULT",
- "media_url": "https://srf-vod-amd.akamaized.net/world/hls/ts20/2023/05/ts20_20230509_124500_18781731_v_webcast_h264_,q40,q10,q20,q30,q50,q60,.mp4.csmil/master.m3u8?caption=srf/dcc5b238-7943-4fe7-8636-e41989d91408/episode/de/vod/vod.m3u8:de:Deutsch:sdh&webvttbaseurl=subtitles.eai-general.aws.srf.ch"
- }
- }
- ],
- "aspectRatio": "16:9",
- "spriteSheet": {
- "urn": "urn:srf:video:f10ba470-6a3c-4479-8b2a-4529f7066234",
- "rows": 18,
- "columns": 20,
- "thumbnailHeight": 84,
- "thumbnailWidth": 150,
- "interval": 2000,
- "url": "https://il.srgssr.ch/spritesheet/urn/srf/video/f10ba470-6a3c-4479-8b2a-4529f7066234/sprite-f10ba470-6a3c-4479-8b2a-4529f7066234.jpeg"
- }
- }
- ],
- "topicList": [
- {
- "id": "a709c610-b275-4c0c-a496-cba304c36712",
- "vendor": "SRF",
- "transmission": "TV",
- "urn": "urn:srf:topic:tv:a709c610-b275-4c0c-a496-cba304c36712",
- "title": "News",
- "lead": "Hier finden Sie alle Sendungen von SRF zum Themenbereich News: aktuell, informativ und tiefgründig.",
- "description": "Tagesschau, Meteo, 10vor10, Schweiz aktuell, Börse, Kinder News, Forward - die News-Sendungen von SRF stehen für seriös recherchierte Berichterstattung und kompetente Hintergrundberichterstattung."
- }
- ],
- "analyticsData": {
- "srg_pr_id": "dcc5b238-7943-4fe7-8636-e41989d91408",
- "srg_plid": "ff969c14-c5a7-44ab-ab72-14d4c9e427a9",
- "ns_st_pl": "Tagesschau",
- "ns_st_pr": "Tagesschau vom 09.05.2023",
- "ns_st_dt": "2023-05-09",
- "ns_st_ddt": "2023-05-09",
- "ns_st_tdt": "2023-05-09",
- "ns_st_tm": "12:45:00",
- "ns_st_tep": "2061009989527",
- "ns_st_li": "0",
- "ns_st_stc": "0866",
- "ns_st_st": "SRF Online",
- "ns_st_tpr": "ff969c14-c5a7-44ab-ab72-14d4c9e427a9",
- "ns_st_en": "*null",
- "ns_st_ge": "*null",
- "ns_st_ia": "*null",
- "ns_st_ce": "1",
- "ns_st_cdm": "to",
- "ns_st_cmt": "fc",
- "srg_unit": "SRF",
- "srg_c1": "News",
- "srg_c2": "full",
- "srg_wo": "0",
- "srg_tv_id": "1981706492812",
- "srg_fullLength": "full"
- },
- "analyticsMetadata": {
- "media_episode_id": "dcc5b238-7943-4fe7-8636-e41989d91408",
- "media_show_id": "ff969c14-c5a7-44ab-ab72-14d4c9e427a9",
- "media_show": "Tagesschau",
- "media_episode": "Tagesschau vom 09.05.2023",
- "media_is_livestream": "false",
- "media_full_length": "full",
- "media_enterprise_units": "SRF",
- "media_joker1": "News",
- "media_is_web_only": "false",
- "media_production_source": "produced.for.broadcasting",
- "media_tv_id": "2061009989527",
- "media_thumbnail": "https://ws.srf.ch/asset/image/audio/c7f097f3-2738-4bc1-ae41-244399a3fbc9/EPISODE_IMAGE/1683630053.png/scale/width/344",
- "media_publication_date": "2023-05-09",
- "media_publication_time": "12:45:00",
- "media_publication_datetime": "2023-05-09T12:45:00+02:00",
- "media_tv_date": "2023-05-09",
- "media_tv_time": "12:45:00",
- "media_tv_datetime": "2023-05-09T12:45:00+02:00",
- "media_content_group": "News",
- "media_channel_id": "23FFBE1B-65CE-4188-ADD2-C724186C2C9F",
- "media_channel_cs": "0866",
- "media_channel_name": "SRF 1",
- "media_since_publication_d": "2",
- "media_since_publication_h": "69"
- }
- }
-]
diff --git a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/CommandersActTrackerTest.kt b/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/CommandersActTrackerTest.kt
deleted file mode 100644
index ef9d5a3b0..000000000
--- a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/CommandersActTrackerTest.kt
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * Copyright (c) SRG SSR. All rights reserved.
- * License information is available from the LICENSE file.
- */
-package ch.srgssr.pillarbox.core.business
-
-import androidx.media3.common.MediaItem
-import androidx.media3.common.Player
-import androidx.test.filters.FlakyTest
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct
-import ch.srgssr.pillarbox.analytics.commandersact.CommandersActPageView
-import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType
-import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent
-import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository
-import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActStreaming
-import ch.srgssr.pillarbox.player.PillarboxPlayer
-import ch.srgssr.pillarbox.player.test.utils.TestPlayer
-import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
-import kotlin.math.abs
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-
-class CommandersActTrackerTest {
- private lateinit var commandersActDelegate: CommandersActDelegate
-
- @Before
- fun setup() {
- CommandersActStreaming.HEART_BEAT_DELAY = HEART_BEAT_DELAY
- CommandersActStreaming.UPTIME_PERIOD = UPTIME_PERIOD
- CommandersActStreaming.POS_PERIOD = POS_PERIOD
- commandersActDelegate = CommandersActDelegate()
- }
-
- private suspend fun createPlayerWithUrn(urn: String, playWhenReady: Boolean = true): TestPlayer {
- val context = getInstrumentation().targetContext
- val player = PillarboxPlayer(
- context = context,
- mediaItemSource = MediaCompositionMediaItemSource(
- mediaCompositionDataSource = LocalMediaCompositionDataSource(context),
- ),
- mediaItemTrackerProvider = DefaultMediaItemTrackerRepository(
- trackerRepository = MediaItemTrackerRepository(),
- commandersAct = commandersActDelegate
- )
- )
- player.volume = 0.0f
- player.setMediaItem(MediaItem.Builder().setMediaId(urn).build())
- player.playWhenReady = playWhenReady
- val testPlayer = TestPlayer(player)
- testPlayer.prepare()
- return testPlayer
- }
-
- @Test
- fun testStartEoF() = runTest {
- val expected = listOf(
- MediaEventType.Play.toString(),
- MediaEventType.Eof.toString()
- )
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.VodShort)
- player.waitForCondition {
- it.playbackState == Player.STATE_ENDED || it.playbackState == Player.STATE_IDLE
- }
- player.release()
- Assert.assertEquals(expected, commandersActDelegate.eventNames)
- }
- }
-
- @Test
- fun testPlayStop() = runTest {
- val expected = listOf(
- MediaEventType.Play.toString(),
- MediaEventType.Stop.toString()
- )
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod)
- player.release()
- Assert.assertEquals(expected, commandersActDelegate.eventNames)
- }
- }
-
- @Test
- fun testPlaySeekPlay() = runTest {
- val seekPositionMs = 2_000L
- val expectedEvents = listOf(
- CommandersActDelegate.Event(MediaEventType.Play.toString(), 0L),
- CommandersActDelegate.Event(MediaEventType.Seek.toString(), 0L),
- CommandersActDelegate.Event(MediaEventType.Play.toString(), seekPositionMs.milliseconds.inWholeSeconds),
- CommandersActDelegate.Event(MediaEventType.Stop.toString())
- )
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod)
- player.seekTo(seekPositionMs)
- player.release()
- Assert.assertEquals(expectedEvents, commandersActDelegate.events)
- }
- }
-
- /**
- * Test pause play seek play
- * Seek event is not send but play event position should be the seek position.
- */
- @Test
- fun testPausePlaySeekPlay() = runTest {
- val seekPositionMs = 2_000L
- val expected = listOf(
- CommandersActDelegate.Event(MediaEventType.Play.toString(), seekPositionMs.milliseconds.inWholeSeconds),
- CommandersActDelegate.Event(MediaEventType.Stop.toString())
- )
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod, false)
- player.play()
- player.seekTo(seekPositionMs)
- player.release()
- Assert.assertEquals(expected, commandersActDelegate.events)
- }
- }
-
- @Test
- fun testPlayPauseSeekPause() = runTest {
- val seekPositionMs = 4_000L
- val expected = listOf(
- MediaEventType.Play.toString(),
- MediaEventType.Pause.toString(),
- MediaEventType.Stop.toString()
- )
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod)
- delay(2_000)
- player.pause()
- delay(2_000)
- player.seekTo(seekPositionMs)
- delay(2_000)
- player.release()
- Assert.assertEquals(expected, commandersActDelegate.eventNames)
- }
- }
-
- @FlakyTest(detail = "POS and UPTIME not always send due to timers")
- @Test
- fun testPosTime() = runTest {
- val expected = listOf(
- MediaEventType.Pos.toString(),
- MediaEventType.Pos.toString(),
- )
- commandersActDelegate.ignorePeriodicEvents = false
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod)
- delay(POS_PERIOD + HEART_BEAT_DELAY + DELTA_PERIOD)
- Assert.assertEquals(false, player.player.isCurrentMediaItemLive)
- player.release()
- val sent = commandersActDelegate.eventNames.filter { it == MediaEventType.Pos.toString() }
- Assert.assertTrue(sent.size >= expected.size)
- }
- }
-
- @FlakyTest(detail = "POS and UPTIME not always send due to timers")
- @Test
- fun testUpTime() = runTest {
- val expected = listOf(
- MediaEventType.Uptime.toString(),
- MediaEventType.Uptime.toString(),
- )
-
- commandersActDelegate.ignorePeriodicEvents = false
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Live)
- delay(UPTIME_PERIOD + HEART_BEAT_DELAY + DELTA_PERIOD)
- player.release()
- val sent = commandersActDelegate.eventNames.filter { it == MediaEventType.Uptime.toString() }
- Assert.assertTrue(sent.size >= expected.size)
- }
- }
-
- @FlakyTest(detail = "POS and UPTIME not always send due to timers")
- @Test
- fun testUpTimeLiveWithDvr() = runTest {
- val expected = listOf(
- MediaEventType.Uptime.toString(),
- MediaEventType.Uptime.toString(),
- )
- commandersActDelegate.ignorePeriodicEvents = false
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Dvr)
- delay(UPTIME_PERIOD + HEART_BEAT_DELAY + DELTA_PERIOD)
- player.release()
- val sent = commandersActDelegate.eventNames.filter { it == MediaEventType.Uptime.toString() }
- Assert.assertTrue(sent.size >= expected.size)
- }
- }
-
- @FlakyTest
- @Test
- fun testUpTimeLiveWithDvrTimeShift() = runTest {
- val seekPosition = 80.seconds
- commandersActDelegate.ignorePeriodicEvents = false
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Dvr)
- val timeshift = (player.player.duration.milliseconds - seekPosition).inWholeSeconds
- player.seekTo(seekPosition.inWholeMilliseconds)
- delay(UPTIME_PERIOD + HEART_BEAT_DELAY + DELTA_PERIOD)
- player.release()
- val actualTimeshift = commandersActDelegate.events.first {
- it.name == MediaEventType.Pos.toString() || it.name == MediaEventType.Uptime.toString()
- }.timeshift
- Assert.assertFalse(commandersActDelegate.events.isEmpty())
- Assert.assertTrue("Timeshift expected $timeshift but was $actualTimeshift", abs(timeshift - actualTimeshift) <= 15)
- }
- }
-
- @Test
- fun testPauseSeekPause() = runTest {
- val seekPositionMs = 4_000L
- launch(Dispatchers.Main) {
- val player = createPlayerWithUrn(LocalMediaCompositionDataSource.Vod, false)
- player.seekTo(seekPositionMs)
- player.release()
- Assert.assertTrue(commandersActDelegate.eventNames.isEmpty())
- }
- }
-
- internal class CommandersActDelegate(
- var ignorePeriodicEvents: Boolean = true,
- ) :
- CommandersAct {
- data class Event(
- val name: String,
- val position: Long = 0L,
- val timeshift: Long = 0L
- ) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Event
-
- if (name != other.name) return false
- if (abs(position - other.position) > 1) return false
- return true
- }
-
- override fun hashCode(): Int {
- var result = name.hashCode()
- result = 31 * result + position.hashCode()
- return result
- }
- }
-
- val eventNames = ArrayList()
- val events = ArrayList()
-
- override fun sendTcMediaEvent(event: TCMediaEvent) {
- if (event.isPeriodicEvent() && ignorePeriodicEvents) return
- eventNames.add(event.name)
- var position = 0L
- var timeshift = 0L
- if (!event.isEndEvent()) {
- position = event.mediaPosition.inWholeSeconds
- timeshift = event.timeShift?.inWholeSeconds ?: 0L
- }
- events.add(Event(name = event.name, position = position, timeshift = timeshift))
- }
-
- override fun putPermanentData(labels: Map) {
- // Nothing
- }
-
- override fun removePermanentData(label: String) {
- // Nothing
- }
-
- override fun getPermanentDataLabel(label: String): String? {
- // Nothing
- return null
- }
-
- override fun sendPageView(pageView: CommandersActPageView) {
- // Ignored
- }
-
- override fun setConsentServices(consentServices: List) {
- // Nothing
- }
-
- override fun sendEvent(event: ch.srgssr.pillarbox.analytics.commandersact.CommandersActEvent) {
- // Ignored
- }
- }
-
- companion object {
- private val HEART_BEAT_DELAY = 3.seconds
- private val UPTIME_PERIOD = 6.seconds
- private val POS_PERIOD = 3.seconds
- private val DELTA_PERIOD = 500.milliseconds
-
- private fun TCMediaEvent.isPeriodicEvent(): Boolean {
- return eventType == MediaEventType.Pos || eventType == MediaEventType.Uptime
- }
-
- private fun TCMediaEvent.isEndEvent(): Boolean {
- return eventType == MediaEventType.Stop || eventType == MediaEventType.Eof
- }
- }
-}
diff --git a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt b/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt
deleted file mode 100644
index 7a295275b..000000000
--- a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) SRG SSR. All rights reserved.
- * License information is available from the LICENSE file.
- */
-package ch.srgssr.pillarbox.core.business
-
-import android.content.Context
-import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
-import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultHttpClient
-import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionDataSource
-
-class LocalMediaCompositionDataSource(context: Context) : MediaCompositionDataSource {
- private val localData = HashMap()
-
- init {
- val json = context.assets.open("media-compositions.json").bufferedReader().use { it.readText() }
- val listMediaComposition: List = DefaultHttpClient.jsonSerializer.decodeFromString(json)
- for (mediaComposition in listMediaComposition) {
- localData[mediaComposition.mainChapter.urn] = mediaComposition
- }
- }
-
- override suspend fun getMediaCompositionByUrn(urn: String): Result {
- return localData[urn]?.let {
- Result.success(it)
- } ?: Result.failure(IllegalArgumentException("$urn not found!"))
- }
-
- companion object {
- const val Live = "urn:rts:video:8841634"
- const val Dvr = "urn:rts:audio:3262363"
-
- /**
- * Vod, ~ 11 min 52 seconds
- */
- const val Vod = "urn:srf:video:f10ba470-6a3c-4479-8b2a-4529f7066234"
-
- /**
- * Vod short, ~ 10 seconds
- */
- const val VodShort = "urn:rts:video:13444428"
- }
-}
diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt
index 6ad366f02..8aed06e4a 100644
--- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt
+++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt
@@ -14,27 +14,29 @@ import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker
import ch.srgssr.pillarbox.player.tracker.MediaItemTracker
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository
+import kotlinx.coroutines.Dispatchers
+import kotlin.coroutines.CoroutineContext
/**
* Default media item tracker repository for SRG.
*
* @param trackerRepository The MediaItemTrackerRepository to use to store Tracker.Factory.
* @param commandersAct CommanderAct instance to use for tracking. If set to null no tracking is made.
+ * @param coroutineContext The coroutine context in which to track the events.
*/
class DefaultMediaItemTrackerRepository internal constructor(
private val trackerRepository: MediaItemTrackerRepository,
- commandersAct: CommandersAct?
-) :
- MediaItemTrackerProvider by
- trackerRepository {
+ commandersAct: CommandersAct?,
+ coroutineContext: CoroutineContext,
+) : MediaItemTrackerProvider by trackerRepository {
init {
registerFactory(SRGEventLoggerTracker::class.java, SRGEventLoggerTracker.Factory())
registerFactory(ComScoreTracker::class.java, ComScoreTracker.Factory())
val commanderActOrEmpty = commandersAct ?: EmptyCommandersAct
- registerFactory(CommandersActTracker::class.java, CommandersActTracker.Factory(commanderActOrEmpty))
+ registerFactory(CommandersActTracker::class.java, CommandersActTracker.Factory(commanderActOrEmpty, coroutineContext))
}
- constructor() : this(trackerRepository = MediaItemTrackerRepository(), SRGAnalytics.commandersAct)
+ constructor() : this(trackerRepository = MediaItemTrackerRepository(), SRGAnalytics.commandersAct, Dispatchers.Default)
/**
* Register factory
diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt
index 6b105c015..7cdb15d1a 100644
--- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt
+++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt
@@ -16,11 +16,18 @@ import ch.srgssr.pillarbox.core.business.tracker.TotalPlaytimeCounter
import ch.srgssr.pillarbox.player.extension.audio
import ch.srgssr.pillarbox.player.extension.isForced
import ch.srgssr.pillarbox.player.utils.DebugLogger
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import java.util.Timer
-import kotlin.concurrent.fixedRateTimer
-import kotlin.concurrent.scheduleAtFixedRate
+import kotlin.coroutines.CoroutineContext
import kotlin.math.abs
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
@@ -31,7 +38,8 @@ import kotlin.time.Duration.Companion.seconds
internal class CommandersActStreaming(
private val commandersAct: CommandersAct,
private val player: ExoPlayer,
- var currentData: CommandersActTracker.Data
+ var currentData: CommandersActTracker.Data,
+ private val coroutineContext: CoroutineContext,
) : AnalyticsListener {
private enum class State {
@@ -39,7 +47,7 @@ internal class CommandersActStreaming(
}
private var state: State = State.Idle
- private var heartBeatTimer: Timer? = null
+ private var heartBeatJob: Job? = null
private val playtimeTracker = TotalPlaytimeCounter()
init {
@@ -51,27 +59,51 @@ internal class CommandersActStreaming(
private fun startHeartBeat() {
stopHeartBeat()
- heartBeatTimer =
- fixedRateTimer(
- name = "pillarbox-heart-beat", false, initialDelay = HEART_BEAT_DELAY.inWholeMilliseconds,
- period = POS_PERIOD.inWholeMilliseconds
- ) {
- runBlocking(Dispatchers.Main) {
- notifyPos(player.currentPosition.milliseconds)
- }
- }.also {
- if (!player.isCurrentMediaItemLive) return@also
- it.scheduleAtFixedRate(HEART_BEAT_DELAY.inWholeMilliseconds, period = UPTIME_PERIOD.inWholeMilliseconds) {
- runBlocking(Dispatchers.Main) {
- notifyUptime(player.currentPosition.milliseconds)
+
+ heartBeatJob = CoroutineScope(coroutineContext).launch(CoroutineName("pillarbox-heart-beat")) {
+ val posUpdate = periodicTask(
+ period = POS_PERIOD,
+ task = ::notifyPos,
+ )
+ val uptimeUpdate = periodicTask(
+ period = UPTIME_PERIOD,
+ continueLooping = { runOnMain(player::isCurrentMediaItemLive) },
+ task = ::notifyUptime,
+ )
+
+ awaitAll(posUpdate, uptimeUpdate)
+ }
+ }
+
+ private fun CoroutineScope.periodicTask(
+ period: Duration,
+ continueLooping: () -> Boolean = { true },
+ task: (currentPosition: Duration) -> Unit
+ ): Deferred {
+ return async {
+ delay(HEART_BEAT_DELAY)
+
+ while (isActive && continueLooping()) {
+ runOnMain {
+ if (player.playWhenReady) {
+ task(player.currentPosition.milliseconds)
}
}
+
+ delay(period)
}
+ }
+ }
+
+ private fun runOnMain(callback: () -> T): T {
+ return runBlocking(Dispatchers.Main) {
+ callback()
+ }
}
private fun stopHeartBeat() {
- heartBeatTimer?.cancel()
- heartBeatTimer = null
+ heartBeatJob?.cancel()
+ heartBeatJob = null
}
override fun onIsPlayingChanged(eventTime: AnalyticsListener.EventTime, isPlaying: Boolean) {
diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt
index ff056c2d1..61efa63d6 100644
--- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt
+++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt
@@ -7,6 +7,7 @@ package ch.srgssr.pillarbox.core.business.tracker.commandersact
import androidx.media3.exoplayer.ExoPlayer
import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct
import ch.srgssr.pillarbox.player.tracker.MediaItemTracker
+import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -15,8 +16,12 @@ import kotlin.time.Duration.Companion.milliseconds
* https://confluence.srg.beecollaboration.com/display/INTFORSCHUNG/standard+streaming+events%3A+sequence+of+events+for+media+player+actions
*
* @param commandersAct CommandersAct to send stream events
+ * @param coroutineContext The coroutine context in which to track the events
*/
-class CommandersActTracker(private val commandersAct: CommandersAct) : MediaItemTracker {
+class CommandersActTracker(
+ private val commandersAct: CommandersAct,
+ private val coroutineContext: CoroutineContext,
+) : MediaItemTracker {
/**
* Data for CommandersAct
*
@@ -33,7 +38,12 @@ class CommandersActTracker(private val commandersAct: CommandersAct) : MediaItem
require(initialData is Data)
commandersAct.enableRunningInBackground()
currentData = initialData
- analyticsStreaming = CommandersActStreaming(commandersAct = commandersAct, player = player, currentData = initialData)
+ analyticsStreaming = CommandersActStreaming(
+ commandersAct = commandersAct,
+ player = player,
+ currentData = initialData,
+ coroutineContext = coroutineContext,
+ )
analyticsStreaming?.let {
player.addAnalyticsListener(it)
}
@@ -58,9 +68,12 @@ class CommandersActTracker(private val commandersAct: CommandersAct) : MediaItem
/**
* Factory
*/
- class Factory(private val commandersAct: CommandersAct) : MediaItemTracker.Factory {
+ class Factory(
+ private val commandersAct: CommandersAct,
+ private val coroutineContext: CoroutineContext,
+ ) : MediaItemTracker.Factory {
override fun create(): MediaItemTracker {
- return CommandersActTracker(commandersAct)
+ return CommandersActTracker(commandersAct, coroutineContext)
}
}
}
diff --git a/pillarbox-core-business/src/test/assets/media-composition.json b/pillarbox-core-business/src/test/assets/media-composition.json
new file mode 100644
index 000000000..b711fd638
--- /dev/null
+++ b/pillarbox-core-business/src/test/assets/media-composition.json
@@ -0,0 +1,147 @@
+{
+ "chapterUrn": "urn:rts:audio:3262363",
+ "episode": {
+ "id": "3262367",
+ "title": "Couleur 3 en direct",
+ "publishedDate": "2011-07-11T14:20:07+02:00",
+ "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
+ "imageTitle": "Chaîne Couleur 3"
+ },
+ "show": {
+ "id": "3262370",
+ "vendor": "RTS",
+ "transmission": "RADIO",
+ "urn": "urn:rts:show:radio:3262370",
+ "title": "Couleur 3 en direct",
+ "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
+ "imageTitle": "Chaîne Couleur 3",
+ "bannerImageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/3x1",
+ "posterImageUrl": "https://ws.srf.ch/asset/image/audio/e0322b37-5697-474d-93ac-19a4044a6a24/POSTER.jpg",
+ "posterImageIsFallbackUrl": true,
+ "primaryChannelId": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
+ "primaryChannelUrn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
+ "audioDescriptionAvailable": false,
+ "subtitlesAvailable": false,
+ "multiAudioLanguagesAvailable": false,
+ "allowIndexing": false
+ },
+ "channel": {
+ "id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
+ "vendor": "RTS",
+ "urn": "urn:rts:channel:radio:8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
+ "title": "Couleur 3",
+ "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
+ "imageTitle": "Chaîne Couleur 3",
+ "transmission": "RADIO"
+ },
+ "chapterList": [
+ {
+ "id": "3262363",
+ "mediaType": "AUDIO",
+ "vendor": "RTS",
+ "urn": "urn:rts:audio:3262363",
+ "title": "Couleur 3 en direct",
+ "imageUrl": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9",
+ "imageTitle": "Chaîne Couleur 3",
+ "type": "LIVESTREAM",
+ "date": "2011-07-11T14:20:07+02:00",
+ "duration": 0,
+ "playableAbroad": true,
+ "displayable": true,
+ "position": 0,
+ "noEmbed": false,
+ "analyticsMetadata": {
+ "media_segment": "Livestream",
+ "media_type": "Audio",
+ "media_segment_id": "3262363",
+ "media_episode_length": "0",
+ "media_segment_length": "0",
+ "media_number_of_segment_selected": "1",
+ "media_number_of_segments_total": "1",
+ "media_duration_category": "infinit.livestream",
+ "media_is_geoblocked": "false",
+ "media_is_web_only": "false",
+ "media_production_source": "produced.for.broadcasting",
+ "media_urn": "urn:rts:audio:3262363"
+ },
+ "fullLengthMarkIn": 0,
+ "fullLengthMarkOut": 0,
+ "resourceList": [
+ {
+ "url": "http://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8?",
+ "quality": "HD",
+ "protocol": "HLS-DVR",
+ "encoding": "H264",
+ "mimeType": "application/x-mpegURL",
+ "presentation": "DEFAULT",
+ "streaming": "HLS",
+ "dvr": true,
+ "live": true,
+ "mediaContainer": "MPEG2_TS",
+ "audioCodec": "AAC",
+ "videoCodec": "NONE",
+ "tokenType": "NONE",
+ "analyticsMetadata": {
+ "media_streaming_quality": "HD",
+ "media_special_format": "DEFAULT",
+ "media_url": "http://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8?"
+ },
+ "streamOffset": 55000
+ }
+ ]
+ }
+ ],
+ "analyticsData": {
+ "srg_pr_id": "3262367",
+ "srg_plid": "3262370",
+ "ns_st_pl": "Livestream",
+ "ns_st_pr": "Couleur 3 en direct",
+ "ns_st_dt": "2011-07-11",
+ "ns_st_ddt": "2011-07-11",
+ "ns_st_tdt": "2011-07-11",
+ "ns_st_tm": "14:20:07",
+ "ns_st_tep": "*null",
+ "ns_st_li": "1",
+ "ns_st_stc": "0867",
+ "ns_st_st": "Couleur 3",
+ "ns_st_tpr": "11562086",
+ "ns_st_en": "*null",
+ "ns_st_ge": "*null",
+ "ns_st_ia": "*null",
+ "ns_st_ce": "1",
+ "ns_st_cdm": "to",
+ "ns_st_cmt": "fc",
+ "srg_unit": "RTS",
+ "srg_c1": "live",
+ "srg_c2": "rts.ch_audio_couleur3",
+ "srg_c3": "COULEUR 3",
+ "srg_aod_prid": "3262367"
+ },
+ "analyticsMetadata": {
+ "media_episode_id": "3262367",
+ "media_show_id": "11562086",
+ "media_show": "Oui Mais Non",
+ "media_episode": "Couleur 3 en direct",
+ "media_is_livestream": "true",
+ "media_full_length": "full",
+ "media_enterprise_units": "RTS",
+ "media_joker1": "live",
+ "media_joker2": "rts.ch_audio_couleur3",
+ "media_joker3": "COULEUR 3",
+ "media_is_web_only": "false",
+ "media_production_source": "produced.for.broadcasting",
+ "media_thumbnail": "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9/scale/width/344",
+ "media_publication_date": "2011-07-11",
+ "media_publication_time": "14:20:07",
+ "media_publication_datetime": "2011-07-11T14:20:07+02:00",
+ "media_tv_date": "2011-07-11",
+ "media_tv_time": "14:20:07",
+ "media_tv_datetime": "2011-07-11T14:20:07+02:00",
+ "media_content_group": "Couleur 3",
+ "media_channel_id": "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7",
+ "media_channel_cs": "0867",
+ "media_channel_name": "Couleur 3",
+ "media_since_publication_d": "4322",
+ "media_since_publication_h": "103747"
+ }
+}
diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt
index 9d7574613..b539cd19e 100644
--- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt
+++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt
@@ -10,6 +10,7 @@ import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository
import io.mockk.mockk
import io.mockk.verifySequence
+import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Test
class DefaultMediaItemTrackerRepositoryTest {
@@ -21,6 +22,7 @@ class DefaultMediaItemTrackerRepositoryTest {
DefaultMediaItemTrackerRepository(
trackerRepository = trackerRepository,
commandersAct = commandersAct,
+ coroutineContext = EmptyCoroutineContext,
)
verifySequence {
@@ -29,6 +31,7 @@ class DefaultMediaItemTrackerRepositoryTest {
trackerRepository.registerFactory(CommandersActTracker::class.java, any(CommandersActTracker.Factory::class))
}
}
+
@Test
fun `DefaultMediaItemTrackerRepository registers some default factories without CommandersAct`() {
val trackerRepository = mockk(relaxed = true)
@@ -36,6 +39,7 @@ class DefaultMediaItemTrackerRepositoryTest {
DefaultMediaItemTrackerRepository(
trackerRepository = trackerRepository,
commandersAct = null,
+ coroutineContext = EmptyCoroutineContext,
)
verifySequence {
diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt
index 7da7d45b1..def8f428e 100644
--- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt
+++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt
@@ -25,6 +25,7 @@ import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
+import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -43,6 +44,7 @@ class CommandersActStreamingTest {
commandersAct = commandersAct,
player = createExoPlayer(isPlaying = false),
currentData = CommandersActTracker.Data(assets = emptyMap()),
+ coroutineContext = EmptyCoroutineContext,
)
verify {
@@ -81,6 +83,7 @@ class CommandersActStreamingTest {
),
sourceId = "source_id",
),
+ coroutineContext = EmptyCoroutineContext,
)
verify {
@@ -148,6 +151,7 @@ class CommandersActStreamingTest {
),
sourceId = "source_id",
),
+ coroutineContext = EmptyCoroutineContext,
)
verify {
diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt
index cb925266e..cc7331d2c 100644
--- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt
+++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt
@@ -4,6 +4,8 @@
*/
package ch.srgssr.pillarbox.core.business.tracker.commandersact
+import android.content.Context
+import android.os.Looper
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
@@ -12,13 +14,22 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct
-import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Eof
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Pause
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Play
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Pos
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Seek
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Stop
+import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Uptime
import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent
import ch.srgssr.pillarbox.core.business.DefaultPillarbox
import ch.srgssr.pillarbox.core.business.MediaCompositionMediaItemSource
import ch.srgssr.pillarbox.core.business.MediaItemUrn
+import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
import ch.srgssr.pillarbox.core.business.integrationlayer.data.isValidMediaUrn
+import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultHttpClient
import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultMediaCompositionDataSource
+import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionDataSource
import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository
import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker
import ch.srgssr.pillarbox.player.data.MediaItemSource
@@ -30,13 +41,25 @@ import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.mockk.verifyOrder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+import kotlin.math.abs
+import kotlin.test.AfterTest
import kotlin.test.BeforeTest
-import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -45,22 +68,29 @@ class CommandersActTrackerIntegrationTest {
private lateinit var clock: FakeClock
private lateinit var commandersAct: CommandersAct
private lateinit var player: ExoPlayer
+ private lateinit var testDispatcher: TestDispatcher
@BeforeTest
+ @OptIn(ExperimentalCoroutinesApi::class)
fun setup() {
clock = FakeClock(true)
commandersAct = mockk(relaxed = true)
+ testDispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(testDispatcher)
+
+ val context = ApplicationProvider.getApplicationContext()
val mediaItemTrackerRepository = DefaultMediaItemTrackerRepository(
trackerRepository = MediaItemTrackerRepository(),
commandersAct = commandersAct,
+ coroutineContext = testDispatcher,
)
mediaItemTrackerRepository.registerFactory(ComScoreTracker::class.java) {
mockk(relaxed = true)
}
val urnMediaItemSource = MediaCompositionMediaItemSource(
- mediaCompositionDataSource = DefaultMediaCompositionDataSource()
+ mediaCompositionDataSource = LocalMediaCompositionWithFallbackDataSource(context)
)
val mediaItemSource = object : MediaItemSource {
override suspend fun loadMediaItem(mediaItem: MediaItem): MediaItem {
@@ -73,13 +103,23 @@ class CommandersActTrackerIntegrationTest {
}
player = DefaultPillarbox(
- context = ApplicationProvider.getApplicationContext(),
+ context = context,
mediaItemTrackerRepository = mediaItemTrackerRepository,
mediaItemSource = mediaItemSource,
clock = clock,
)
}
+ @AfterTest
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun tearDown() {
+ player.release()
+
+ shadowOf(Looper.getMainLooper()).idle()
+
+ Dispatchers.resetMain()
+ }
+
@Test
fun `player unprepared`() {
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE)
@@ -115,15 +155,15 @@ class CommandersActTrackerIntegrationTest {
assertEquals(3, tcMediaEvents.size)
- assertEquals(MediaEventType.Play, tcMediaEvents[0].eventType)
+ assertEquals(Play, tcMediaEvents[0].eventType)
assertTrue(tcMediaEvents[0].assets.isNotEmpty())
assertNull(tcMediaEvents[0].sourceId)
- assertEquals(MediaEventType.Stop, tcMediaEvents[1].eventType)
+ assertEquals(Stop, tcMediaEvents[1].eventType)
assertTrue(tcMediaEvents[1].assets.isNotEmpty())
assertNull(tcMediaEvents[1].sourceId)
- assertEquals(MediaEventType.Play, tcMediaEvents[2].eventType)
+ assertEquals(Play, tcMediaEvents[2].eventType)
assertTrue(tcMediaEvents[2].assets.isNotEmpty())
assertNull(tcMediaEvents[2].sourceId)
}
@@ -147,7 +187,7 @@ class CommandersActTrackerIntegrationTest {
val tcMediaEvent = tcMediaEventSlot.captured
- assertEquals(MediaEventType.Play, tcMediaEvent.eventType)
+ assertEquals(Play, tcMediaEvent.eventType)
assertTrue(tcMediaEvent.assets.isNotEmpty())
assertNull(tcMediaEvent.sourceId)
}
@@ -197,7 +237,7 @@ class CommandersActTrackerIntegrationTest {
val tcMediaEvent = tcMediaEventSlot.captured
- assertEquals(MediaEventType.Play, tcMediaEvent.eventType)
+ assertEquals(Play, tcMediaEvent.eventType)
assertTrue(tcMediaEvent.assets.isNotEmpty())
assertNull(tcMediaEvent.sourceId)
}
@@ -222,7 +262,7 @@ class CommandersActTrackerIntegrationTest {
val tcMediaEvent = tcMediaEventSlot.captured
- assertEquals(MediaEventType.Play, tcMediaEvent.eventType)
+ assertEquals(Play, tcMediaEvent.eventType)
assertTrue(tcMediaEvent.assets.isNotEmpty())
assertNull(tcMediaEvent.sourceId)
}
@@ -251,7 +291,7 @@ class CommandersActTrackerIntegrationTest {
val tcMediaEvent = tcMediaEventSlot.captured
- assertEquals(MediaEventType.Play, tcMediaEvent.eventType)
+ assertEquals(Play, tcMediaEvent.eventType)
assertTrue(tcMediaEvent.assets.isNotEmpty())
assertNull(tcMediaEvent.sourceId)
}
@@ -282,11 +322,11 @@ class CommandersActTrackerIntegrationTest {
assertEquals(2, tcMediaEvents.size)
- assertEquals(MediaEventType.Pause, tcMediaEvents[0].eventType)
+ assertEquals(Pause, tcMediaEvents[0].eventType)
assertTrue(tcMediaEvents[0].assets.isNotEmpty())
assertNull(tcMediaEvents[0].sourceId)
- assertEquals(MediaEventType.Play, tcMediaEvents[1].eventType)
+ assertEquals(Play, tcMediaEvents[1].eventType)
assertTrue(tcMediaEvents[1].assets.isNotEmpty())
assertNull(tcMediaEvents[1].sourceId)
}
@@ -324,15 +364,15 @@ class CommandersActTrackerIntegrationTest {
assertEquals(3, tcMediaEvents.size)
- assertEquals(MediaEventType.Play, tcMediaEvents[0].eventType)
+ assertEquals(Play, tcMediaEvents[0].eventType)
assertTrue(tcMediaEvents[0].assets.isNotEmpty())
assertNull(tcMediaEvents[0].sourceId)
- assertEquals(MediaEventType.Pause, tcMediaEvents[1].eventType)
+ assertEquals(Pause, tcMediaEvents[1].eventType)
assertTrue(tcMediaEvents[1].assets.isNotEmpty())
assertNull(tcMediaEvents[1].sourceId)
- assertEquals(MediaEventType.Play, tcMediaEvents[2].eventType)
+ assertEquals(Play, tcMediaEvents[2].eventType)
assertTrue(tcMediaEvents[2].assets.isNotEmpty())
assertNull(tcMediaEvents[2].sourceId)
}
@@ -362,11 +402,11 @@ class CommandersActTrackerIntegrationTest {
assertEquals(2, tcMediaEvents.size)
- assertEquals(MediaEventType.Stop, tcMediaEvents[0].eventType)
+ assertEquals(Stop, tcMediaEvents[0].eventType)
assertTrue(tcMediaEvents[0].assets.isNotEmpty())
assertNull(tcMediaEvents[0].sourceId)
- assertEquals(MediaEventType.Play, tcMediaEvents[1].eventType)
+ assertEquals(Play, tcMediaEvents[1].eventType)
assertTrue(tcMediaEvents[1].assets.isNotEmpty())
assertNull(tcMediaEvents[1].sourceId)
}
@@ -397,19 +437,128 @@ class CommandersActTrackerIntegrationTest {
assertEquals(3, tcMediaEvents.size)
- assertEquals(MediaEventType.Play, tcMediaEvents[0].eventType)
+ assertEquals(Play, tcMediaEvents[0].eventType)
+ assertTrue(tcMediaEvents[0].assets.isNotEmpty())
+ assertNull(tcMediaEvents[0].sourceId)
+
+ assertEquals(Seek, tcMediaEvents[1].eventType)
+ assertTrue(tcMediaEvents[1].assets.isNotEmpty())
+ assertNull(tcMediaEvents[1].sourceId)
+
+ assertEquals(Play, tcMediaEvents[2].eventType)
+ assertTrue(tcMediaEvents[2].assets.isNotEmpty())
+ assertNull(tcMediaEvents[2].sourceId)
+ }
+
+ @Test
+ fun `player pause, playing, seeking and playing`() {
+ val tcMediaEventSlot = slot()
+
+ player.setMediaItem(MediaItemUrn(URN_NOT_LIVE_VIDEO))
+ player.prepare()
+ player.playWhenReady = false
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+
+ player.play()
+ player.seekTo(30.seconds.inWholeMilliseconds)
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ commandersAct.sendTcMediaEvent(capture(tcMediaEventSlot))
+ }
+ confirmVerified(commandersAct)
+
+ val tcMediaEvent = tcMediaEventSlot.captured
+
+ assertEquals(Play, tcMediaEvent.eventType)
+ assertTrue(tcMediaEvent.assets.isNotEmpty())
+ assertNull(tcMediaEvent.sourceId)
+ }
+
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun `player playing, pause, seeking and pause`() = runTest(testDispatcher) {
+ val tcMediaEvents = mutableListOf()
+
+ player.setMediaItem(MediaItemUrn(URN_NOT_LIVE_VIDEO))
+ player.prepare()
+ player.playWhenReady = true
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPillarboxRunHelper.runUntilStartOfMediaItem(player, 0)
+
+ clock.advanceTime(2.seconds.inWholeMilliseconds)
+ advanceTimeBy(2.seconds)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.pause()
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPlayWhenReady(player, false)
+
+ clock.advanceTime(2.seconds.inWholeMilliseconds)
+ advanceTimeBy(2.seconds)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.seekTo(30.seconds.inWholeMilliseconds)
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ }
+ confirmVerified(commandersAct)
+
+ assertEquals(3, tcMediaEvents.size)
+
+ assertEquals(Pause, tcMediaEvents[0].eventType)
assertTrue(tcMediaEvents[0].assets.isNotEmpty())
assertNull(tcMediaEvents[0].sourceId)
- assertEquals(MediaEventType.Seek, tcMediaEvents[1].eventType)
+ assertEquals(Pos, tcMediaEvents[1].eventType)
assertTrue(tcMediaEvents[1].assets.isNotEmpty())
assertNull(tcMediaEvents[1].sourceId)
- assertEquals(MediaEventType.Play, tcMediaEvents[2].eventType)
+ assertEquals(Play, tcMediaEvents[2].eventType)
assertTrue(tcMediaEvents[2].assets.isNotEmpty())
assertNull(tcMediaEvents[2].sourceId)
}
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun `player pause, seeking and pause`() = runTest(testDispatcher) {
+ player.setMediaItem(MediaItemUrn(URN_NOT_LIVE_VIDEO))
+ player.prepare()
+ player.playWhenReady = false
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+
+ clock.advanceTime(2.seconds.inWholeMilliseconds)
+ advanceTimeBy(2.seconds)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.seekTo(30.seconds.inWholeMilliseconds)
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ }
+ confirmVerified(commandersAct)
+ }
+
@Test
fun `player prepared and seek`() {
player.setMediaItem(MediaItemUrn(URN_NOT_LIVE_VIDEO))
@@ -435,15 +584,15 @@ class CommandersActTrackerIntegrationTest {
verify { commandersAct wasNot Called }
}
- @Ignore("Currently very flaky due to timer.")
@Test
- fun `check uptime and position updates`() {
- val delay = 2.seconds
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun `check uptime and position updates for live`() = runTest(testDispatcher) {
+ val playTime = 10.seconds
val tcMediaEvents = mutableListOf()
- CommandersActStreaming.HEART_BEAT_DELAY = 0.5.seconds
- CommandersActStreaming.POS_PERIOD = 0.5.seconds
- CommandersActStreaming.UPTIME_PERIOD = 1.seconds
+ CommandersActStreaming.HEART_BEAT_DELAY = 1.seconds
+ CommandersActStreaming.POS_PERIOD = 2.seconds
+ CommandersActStreaming.UPTIME_PERIOD = 4.seconds
player.setMediaItem(MediaItemUrn(URN_LIVE_VIDEO))
player.prepare()
@@ -452,13 +601,24 @@ class CommandersActTrackerIntegrationTest {
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
TestPillarboxRunHelper.runUntilStartOfMediaItem(player, 0)
- clock.advanceTime(delay.inWholeMilliseconds)
- Thread.sleep(delay.inWholeMilliseconds)
+ clock.advanceTime(playTime.inWholeMilliseconds)
+ advanceTimeBy(playTime)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
player.playWhenReady = false
TestPlayerRunHelper.runUntilPlayWhenReady(player, false)
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+ // Advance a bit more in time to ensure that no events are sent after pause
+ clock.advanceTime(playTime.inWholeMilliseconds)
+ advanceTimeBy(playTime)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ assertTrue(player.isCurrentMediaItemLive)
+
verifyOrder {
commandersAct.enableRunningInBackground()
commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
@@ -469,42 +629,177 @@ class CommandersActTrackerIntegrationTest {
commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
}
confirmVerified(commandersAct)
- assertEquals(8, tcMediaEvents.size)
+ assertEquals(10, tcMediaEvents.size)
- assertEquals(MediaEventType.Pause, tcMediaEvents[0].eventType)
- assertTrue(tcMediaEvents[0].assets.isNotEmpty())
- assertNull(tcMediaEvents[0].sourceId)
+ assertEquals(listOf(Pause, Pos, Uptime, Pos, Pos, Uptime, Pos, Uptime, Pos, Play), tcMediaEvents.map { it.eventType })
+ assertTrue(tcMediaEvents.all { it.assets.isNotEmpty() })
+ assertTrue(tcMediaEvents.all { it.sourceId == null })
+ }
- assertEquals(MediaEventType.Pos, tcMediaEvents[1].eventType)
- assertTrue(tcMediaEvents[1].assets.isNotEmpty())
- assertNull(tcMediaEvents[1].sourceId)
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun `check uptime and position updates for dvr with time shift`() = runTest(testDispatcher) {
+ val playTime = 5.seconds
+ val seekPosition = 80.seconds
+ val tcMediaEvents = mutableListOf()
- assertEquals(MediaEventType.Uptime, tcMediaEvents[2].eventType)
- assertTrue(tcMediaEvents[2].assets.isNotEmpty())
- assertNull(tcMediaEvents[2].sourceId)
+ CommandersActStreaming.HEART_BEAT_DELAY = 1.seconds
+ CommandersActStreaming.POS_PERIOD = 2.seconds
+ CommandersActStreaming.UPTIME_PERIOD = 4.seconds
+
+ player.setMediaItem(MediaItemUrn(URN_DVR))
+ player.prepare()
+ player.playWhenReady = true
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.seekTo(seekPosition.inWholeMilliseconds)
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ clock.advanceTime(playTime.inWholeMilliseconds)
+ advanceTimeBy(playTime)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.stop()
- assertEquals(MediaEventType.Pos, tcMediaEvents[3].eventType)
- assertTrue(tcMediaEvents[3].assets.isNotEmpty())
- assertNull(tcMediaEvents[3].sourceId)
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ }
+ confirmVerified(commandersAct)
+
+ assertEquals(7, tcMediaEvents.size)
+
+ assertEquals(listOf(Stop, Pos, Uptime, Pos, Play, Seek, Play), tcMediaEvents.map { it.eventType })
+ assertTrue(tcMediaEvents.all { it.assets.isNotEmpty() })
+ assertTrue(tcMediaEvents.all { it.sourceId == null })
- assertEquals(MediaEventType.Pos, tcMediaEvents[4].eventType)
- assertTrue(tcMediaEvents[4].assets.isNotEmpty())
- assertNull(tcMediaEvents[4].sourceId)
+ val timeShift = (player.duration.milliseconds - seekPosition).inWholeSeconds
+ val actualTimeShift = tcMediaEvents.first {
+ it.eventType == Pos || it.eventType == Uptime
+ }.timeShift?.inWholeSeconds ?: 0L
- assertEquals(MediaEventType.Uptime, tcMediaEvents[5].eventType)
- assertTrue(tcMediaEvents[5].assets.isNotEmpty())
- assertNull(tcMediaEvents[5].sourceId)
+ assertTrue(abs(timeShift - actualTimeShift) <= 15L, "Expected time shift to be <$timeShift>, but was <$actualTimeShift>")
+ }
+
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun `check uptime and position updates for not live`() = runTest(testDispatcher) {
+ val playTime = 10.seconds
+ val tcMediaEvents = mutableListOf()
- assertEquals(MediaEventType.Pos, tcMediaEvents[6].eventType)
- assertTrue(tcMediaEvents[6].assets.isNotEmpty())
- assertNull(tcMediaEvents[6].sourceId)
+ CommandersActStreaming.HEART_BEAT_DELAY = 1.seconds
+ CommandersActStreaming.POS_PERIOD = 2.seconds
+ CommandersActStreaming.UPTIME_PERIOD = 4.seconds
- assertEquals(MediaEventType.Play, tcMediaEvents[7].eventType)
- assertTrue(tcMediaEvents[7].assets.isNotEmpty())
- assertNull(tcMediaEvents[7].sourceId)
+ player.setMediaItem(MediaItemUrn(URN_NOT_LIVE_VIDEO))
+ player.prepare()
+ player.playWhenReady = true
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY)
+ TestPillarboxRunHelper.runUntilStartOfMediaItem(player, 0)
+
+ clock.advanceTime(playTime.inWholeMilliseconds)
+ advanceTimeBy(playTime)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ player.playWhenReady = false
+
+ TestPlayerRunHelper.runUntilPlayWhenReady(player, false)
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ // Advance a bit more in time to ensure that no events are sent after pause
+ clock.advanceTime(playTime.inWholeMilliseconds)
+ advanceTimeBy(playTime)
+
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player)
+
+ assertFalse(player.isCurrentMediaItemLive)
+
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ }
+ confirmVerified(commandersAct)
+
+ assertEquals(7, tcMediaEvents.size)
+
+ assertEquals(listOf(Pause, Pos, Pos, Pos, Pos, Pos, Play), tcMediaEvents.map { it.eventType })
+ assertTrue(tcMediaEvents.all { it.assets.isNotEmpty() })
+ assertTrue(tcMediaEvents.all { it.sourceId == null })
+ }
+
+ @Test
+ fun `start EoF`() = runTest(testDispatcher) {
+ val tcMediaEvents = mutableListOf()
+
+ CommandersActStreaming.HEART_BEAT_DELAY = 1.seconds
+ CommandersActStreaming.POS_PERIOD = 2.seconds
+ CommandersActStreaming.UPTIME_PERIOD = 4.seconds
+
+ player.setMediaItem(MediaItemUrn(URN_VOD_SHORT))
+ player.prepare()
+ player.playWhenReady = true
+
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED)
+
+ verifyOrder {
+ commandersAct.enableRunningInBackground()
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ commandersAct.sendTcMediaEvent(capture(tcMediaEvents))
+ }
+ confirmVerified(commandersAct)
+
+ assertEquals(2, tcMediaEvents.size)
+
+ assertEquals(listOf(Eof, Play), tcMediaEvents.map { it.eventType })
+ assertTrue(tcMediaEvents.all { it.assets.isNotEmpty() })
+ assertTrue(tcMediaEvents.all { it.sourceId == null })
+ }
+
+ private class LocalMediaCompositionWithFallbackDataSource(
+ context: Context,
+ private val fallbackDataSource: MediaCompositionDataSource = DefaultMediaCompositionDataSource(),
+ ) : MediaCompositionDataSource {
+ private var mediaComposition: MediaComposition? = null
+
+ init {
+ val json = context.assets.open("media-composition.json").bufferedReader().use { it.readText() }
+
+ mediaComposition = DefaultHttpClient.jsonSerializer.decodeFromString(json)
+ }
+
+ override suspend fun getMediaCompositionByUrn(urn: String): Result {
+ return if (urn == URN_DVR) {
+ runCatching {
+ requireNotNull(mediaComposition)
+ }
+ } else {
+ fallbackDataSource.getMediaCompositionByUrn(urn)
+ }
+ }
}
private companion object {
@@ -512,5 +807,7 @@ class CommandersActTrackerIntegrationTest {
private const val URN_AUDIO = "urn:rts:audio:13598743"
private const val URN_LIVE_VIDEO = "urn:rts:video:8841634"
private const val URN_NOT_LIVE_VIDEO = "urn:rsi:video:15916771"
+ private const val URN_VOD_SHORT = "urn:rts:video:13444428"
+ private const val URN_DVR = "urn:rts:audio:3262363"
}
}
diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt
index e526b196d..af2e2bfe1 100644
--- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt
+++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt
@@ -16,6 +16,7 @@ import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.junit.runner.RunWith
+import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -29,7 +30,7 @@ class CommandersActTrackerTest {
fun `start() requires a non-null initial data`() {
val player = mockk(relaxed = true)
val commandersActs = mockk(relaxed = true)
- val commandersActTracker = CommandersActTracker(commandersActs)
+ val commandersActTracker = CommandersActTracker(commandersActs, EmptyCoroutineContext)
commandersActTracker.start(
player = player,
@@ -41,7 +42,7 @@ class CommandersActTrackerTest {
fun `start() requires an instance of CommandersActTracker#Data instance for the initial data`() {
val player = mockk(relaxed = true)
val commandersActs = mockk(relaxed = true)
- val commandersActTracker = CommandersActTracker(commandersActs)
+ val commandersActTracker = CommandersActTracker(commandersActs, EmptyCoroutineContext)
commandersActTracker.start(
player = player,
@@ -52,7 +53,7 @@ class CommandersActTrackerTest {
@Test(expected = IllegalArgumentException::class)
fun `update() requires an instance of CommandersActTracker#Data instance for the data`() {
val commandersActs = mockk(relaxed = true)
- val commandersActTracker = CommandersActTracker(commandersActs)
+ val commandersActTracker = CommandersActTracker(commandersActs, EmptyCoroutineContext)
commandersActTracker.update(data = "My data")
}
@@ -63,7 +64,7 @@ class CommandersActTrackerTest {
every { isPlaying } returns true
}
val commandersAct = mockk(relaxed = true)
- val commandersActTracker = CommandersActTracker(commandersAct)
+ val commandersActTracker = CommandersActTracker(commandersAct, EmptyCoroutineContext)
val commandersActStreamingSlot = slot()
val tcMediaEventSlots = mutableListOf()
diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt
index 20489d537..3772acacf 100644
--- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt
+++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt
@@ -32,6 +32,7 @@ import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
import org.junit.runner.RunWith
+import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.BeforeTest
import kotlin.test.Ignore
import kotlin.test.Test
@@ -53,6 +54,7 @@ class ComScoreTrackerIntegrationTest {
val mediaItemTrackerRepository = DefaultMediaItemTrackerRepository(
trackerRepository = MediaItemTrackerRepository(),
commandersAct = null,
+ coroutineContext = EmptyCoroutineContext,
)
mediaItemTrackerRepository.registerFactory(ComScoreTracker::class.java) {
ComScoreTracker(streamingAnalytics)
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt
index a68686953..32bc1eb5f 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt
@@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -91,7 +91,7 @@ private fun ListStreamView(
)
if (index < playlist.items.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt
index fd1cefa63..dc581f94f 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -149,7 +149,7 @@ private fun ListsHome(onContentSelected: (ContentList) -> Unit) {
)
if (index < section.contentList.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt
index e4a9cd84f..ab8bb2d91 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt
@@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -114,7 +114,7 @@ fun ListsSubSection(
)
if (index < items.itemCount - 1) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt
index 60f2d48bd..44e5e6c4f 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt
@@ -15,7 +15,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
@@ -94,7 +94,7 @@ private fun DialogContent(
text = "Add to the playlist",
style = MaterialTheme.typography.headlineMedium
)
- Divider()
+ HorizontalDivider()
LazyColumn(
modifier = Modifier
.weight(0.5f)
@@ -113,7 +113,7 @@ private fun DialogContent(
)
}
}
- Divider()
+ HorizontalDivider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt
index 24e76154c..7da112aac 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt
@@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
@@ -44,7 +44,7 @@ fun PlaybackSpeedSettings(
)
if (index < playbackSpeeds.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt
index d98a1d70a..eaabc7414 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt
@@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HearingDisabled
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
@@ -63,7 +63,7 @@ fun TrackSelectionSettings(
)
}
)
- Divider()
+ HorizontalDivider()
}
item {
SettingsOption(
@@ -74,7 +74,7 @@ fun TrackSelectionSettings(
Text(text = stringResource(R.string.disabled))
}
)
- Divider()
+ HorizontalDivider()
}
tracksSetting.tracks.forEach { group ->
items(group.length) { trackIndex ->
@@ -117,7 +117,7 @@ fun TrackSelectionSettings(
)
}
item {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt
index eeab8f564..b1a5951d3 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt
@@ -31,10 +31,10 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -165,7 +165,7 @@ private fun SearchResultList(
)
if (index < items.itemCount - 1) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt
index 4a9141d27..5fb60177a 100644
--- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt
+++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -70,7 +70,7 @@ fun ShowcasesHome(navController: NavController) {
onClick = { navController.navigate(NavigationRoutes.simplePlayer) }
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.story),
@@ -93,7 +93,7 @@ fun ShowcasesHome(navController: NavController) {
)
if (index < playlists.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
@@ -112,7 +112,7 @@ fun ShowcasesHome(navController: NavController) {
)
}
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.auto),
@@ -140,7 +140,7 @@ fun ShowcasesHome(navController: NavController) {
}
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.adaptive),
@@ -148,14 +148,14 @@ fun ShowcasesHome(navController: NavController) {
onClick = { navController.navigate(NavigationRoutes.adaptive) }
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.player_swap),
modifier = itemModifier,
onClick = { navController.navigate(NavigationRoutes.playerSwap) }
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.tracker_example),
@@ -163,7 +163,7 @@ fun ShowcasesHome(navController: NavController) {
onClick = { navController.navigate(NavigationRoutes.trackingSample) }
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.update_media_item_example),
@@ -175,7 +175,7 @@ fun ShowcasesHome(navController: NavController) {
}
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.smooth_seeking_example),
@@ -187,7 +187,7 @@ fun ShowcasesHome(navController: NavController) {
}
)
- Divider()
+ HorizontalDivider()
DemoListItemView(
title = stringResource(R.string.video_360),
diff --git a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/PlayerListenerCommander.kt b/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/PlayerListenerCommander.kt
index ab250d310..d4b244b8a 100644
--- a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/PlayerListenerCommander.kt
+++ b/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/PlayerListenerCommander.kt
@@ -27,12 +27,6 @@ import androidx.media3.common.text.CueGroup
open class PlayerListenerCommander(player: Player) : ForwardingPlayer(player), Listener {
private val listeners = mutableListOf()
- /**
- * Has player listener
- */
- val hasPlayerListener: Boolean
- get() = listeners.isNotEmpty()
-
@SuppressLint("MissingSuperCall")
override fun addListener(listener: Listener) {
listeners.add(listener)
diff --git a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPlayer.kt b/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPlayer.kt
deleted file mode 100644
index 7ce5d93c5..000000000
--- a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPlayer.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) SRG SSR. All rights reserved.
- * License information is available from the LICENSE file.
- */
-package ch.srgssr.pillarbox.player.test.utils
-
-import androidx.media3.common.Player
-import kotlinx.coroutines.delay
-
-class TestPlayer(val player: Player) {
-
- suspend fun prepare() {
- player.prepare()
- player.waitForPlaybackState(Player.STATE_READY)
- }
-
- suspend fun play() {
- player.play()
- player.waitIsPlaying()
- }
-
- suspend fun pause() {
- player.pause()
- player.waitForPause()
- }
-
- suspend fun seekTo(positionMs: Long) {
- player.seekTo(positionMs)
- player.waitForPlaybackState(Player.STATE_READY)
- }
-
- suspend fun release() {
- player.stop()
- player.release()
- player.waitForPlaybackState(Player.STATE_IDLE)
- }
-
- suspend fun stop() {
- player.stop()
- player.waitForPlaybackState(Player.STATE_IDLE)
- }
-
- suspend fun waitForCondition(condition: (Player) -> Boolean) {
- player.waitForCondition(condition)
- }
-
- companion object {
- private const val WAIT_DELAY = 200L
-
- @Suppress("TooGenericExceptionThrown")
- suspend fun Player.waitForCondition(condition: (Player) -> Boolean) {
- while (!condition(this)) {
- if (playerError != null) throw RuntimeException(playerError)
- delay(WAIT_DELAY)
- }
- }
-
- suspend fun Player.waitForPlaybackState(state: @Player.State Int) {
- waitForCondition {
- it.playbackState == state
- }
- }
-
- suspend fun Player.waitForPause() {
- waitForCondition {
- it.playbackState == Player.STATE_READY && !it.playWhenReady
- }
- }
-
- suspend fun Player.waitIsPlaying() {
- waitForCondition {
- it.isPlaying
- }
- }
- }
-}