diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsKind1SiblingBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsKind1SiblingBuilder.kt new file mode 100644 index 000000000..c8d92be3d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsKind1SiblingBuilder.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.hls + +import com.davotoula.lightcompressor.hls.HlsContentTypes +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.references.reference +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip71Video.blurhash +import com.vitorpamplona.quartz.nip71Video.dims +import com.vitorpamplona.quartz.nip71Video.hash +import com.vitorpamplona.quartz.nip71Video.image +import com.vitorpamplona.quartz.nip71Video.mimeType +import com.vitorpamplona.quartz.nip71Video.thumbhash +import com.vitorpamplona.quartz.nip92IMeta.imeta +import com.vitorpamplona.quartz.nip92IMeta.imetaTagBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Builds the kind:1 short-note that's published as a sibling of the NIP-71 video event from + * Amethyst's HLS publish flow. Receivers that don't speak NIP-71 can still render a rich + * preview (poster + dim + blurhash/thumbhash) from the kind:1 alone, while NIP-71-aware + * receivers can hop to the addressable form via the `a` tag. + * + * Tag layout: + * - One `imeta` mirroring the master rendition (url=master m3u8, m=hls, x=master sha256, + * dim=master dim, image=poster, blurhash, thumbhash). + * - One `r` tag with the master m3u8 URL (idiomatic kind:1 reference). + * + * Content mirrors the previous "Draft note" layout: `title\n\ndescription\n\nmasterUrl`, + * blank fields skipped. + * + * Intentionally omits an `a` tag back-reference to the NIP-71 event: in practice Amethyst's + * note renderer prefers the embedded addressable form when an `a` tag is present, and falls + * back to a placeholder if the relay set hasn't returned it yet — masking the rich imeta we + * just added. Without `a`, the kind:1 renders as a vanilla rich-imeta video note across + * clients, which is what users expect from a shared video link. + */ +object HlsKind1SiblingBuilder { + fun build( + title: String, + description: String, + masterUrl: String, + masterSha256: String?, + masterDimension: DimensionTag?, + posterUrl: String?, + blurhashValue: String?, + thumbhashValue: String?, + createdAt: Long? = null, + ): EventTemplate { + val content = + listOf(title, description, masterUrl) + .map { it.trim() } + .filter { it.isNotEmpty() } + .joinToString("\n\n") + + val masterImeta = + imetaTagBuilder(masterUrl) { + mimeType(HlsContentTypes.HLS_PLAYLIST) + masterSha256?.let { hash(it) } + masterDimension?.let { dims(it) } + posterUrl?.let { image(it) } + blurhashValue?.let { blurhash(it) } + thumbhashValue?.let { thumbhash(it) } + } + + return TextNoteEvent.build(content, createdAt ?: TimeUtils.now()) { + imeta(masterImeta) + reference(masterUrl) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt index ce015135b..112f64f89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt @@ -71,6 +71,20 @@ sealed class HlsVideoEventTemplate { ) : HlsVideoEventTemplate() } +/** + * Result of [HlsVideoEventBuilder.build]. Exposes the unsigned NIP-71 [template] plus the + * resolved coordinates the caller needs to construct a matching kind:1 sibling note: the + * orientation [kind] (34235/34236), the addressable [dTag] (replayable into an `a` tag), and + * the master [masterDimension] (largest rendition's WxH) so the kind:1 imeta carries the same + * dim as the NIP-71 master imeta. + */ +data class HlsBuiltTemplate( + val template: HlsVideoEventTemplate, + val kind: Int, + val dTag: String, + val masterDimension: DimensionTag?, +) + /** * Assembles a NIP-71 VideoHorizontalEvent / VideoVerticalEvent template from an HLS upload * result. Orientation is decided from the first rendition's width/height: portrait @@ -86,7 +100,7 @@ sealed class HlsVideoEventTemplate { */ @OptIn(ExperimentalUuidApi::class) object HlsVideoEventBuilder { - fun build(input: HlsVideoPublishInput): HlsVideoEventTemplate { + fun build(input: HlsVideoPublishInput): HlsBuiltTemplate { val firstRendition = input.renditions.firstOrNull() val isVertical = firstRendition != null && firstRendition.height > firstRendition.width @@ -131,24 +145,28 @@ object HlsVideoEventBuilder { val dTag = input.dTag ?: Uuid.random().toString() val createdAt = input.createdAt ?: TimeUtils.now() - return if (isVertical) { - HlsVideoEventTemplate.Vertical( - VideoVerticalEvent.build(input.description, dTag, createdAt) { - videoIMetas(videoMetas) - title(input.title) - input.durationSeconds?.let { duration(it) } - input.contentWarning?.let { contentWarning(it) } - }, - ) - } else { - HlsVideoEventTemplate.Horizontal( - VideoHorizontalEvent.build(input.description, dTag, createdAt) { - videoIMetas(videoMetas) - title(input.title) - input.durationSeconds?.let { duration(it) } - input.contentWarning?.let { contentWarning(it) } - }, - ) - } + val template = + if (isVertical) { + HlsVideoEventTemplate.Vertical( + VideoVerticalEvent.build(input.description, dTag, createdAt) { + videoIMetas(videoMetas) + title(input.title) + input.durationSeconds?.let { duration(it) } + input.contentWarning?.let { contentWarning(it) } + }, + ) + } else { + HlsVideoEventTemplate.Horizontal( + VideoHorizontalEvent.build(input.description, dTag, createdAt) { + videoIMetas(videoMetas) + title(input.title) + input.durationSeconds?.let { duration(it) } + input.contentWarning?.let { contentWarning(it) } + }, + ) + } + + val kind = if (isVertical) VideoVerticalEvent.KIND else VideoHorizontalEvent.KIND + return HlsBuiltTemplate(template, kind, dTag, masterDimension) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt index ae5b4c653..b1e2a8316 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt @@ -31,10 +31,13 @@ import com.davotoula.lightcompressor.hls.Rendition import com.davotoula.lightcompressor.hls.SimpleHlsListener import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.hls.HlsBlobUploader +import com.vitorpamplona.amethyst.service.uploads.hls.HlsKind1SiblingBuilder import com.vitorpamplona.amethyst.service.uploads.hls.HlsVideoEventBuilder import com.vitorpamplona.amethyst.service.uploads.hls.HlsVideoEventTemplate import com.vitorpamplona.amethyst.service.uploads.hls.HlsVideoPublishInput import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow @@ -88,7 +91,15 @@ class HlsPublishOrchestrator( ) -> HlsUploadResult, private val buildUploader: (ServerName) -> HlsBlobUploader, private val uploadMaster: suspend (HlsBlobUploader, String) -> MediaUploadResult, - private val signAndPublish: suspend (HlsVideoEventTemplate) -> String, + // Signs and broadcasts the NIP-71 video event AND the kind:1 sibling note. The + // orchestrator hands the primary template plus a lazily-evaluated [buildSibling] closure + // that produces the kind:1 template — lazy so the orchestrator can capture the master + // metadata in scope without forcing kind:1 construction before the NIP-71 sign. Returns + // the NIP-71 event id. + private val signAndPublish: suspend ( + primary: HlsVideoEventTemplate, + buildSibling: () -> EventTemplate, + ) -> String, // Generates a poster JPEG from the picked source video and uploads it via the supplied // uploader, returning the public URL. Returns null if poster generation isn't possible // (unsupported source, decode failure, no readable frame). Failures here must NOT fail @@ -231,7 +242,7 @@ class HlsPublishOrchestrator( } _state.value = HlsPublishState.Publishing - val template = + val built = HlsVideoEventBuilder.build( HlsVideoPublishInput( renditions = uploadResult.renditions, @@ -247,7 +258,19 @@ class HlsPublishOrchestrator( thumbhash = posterResult?.thumbhash, ), ) - val eventId = signAndPublish(template) + val buildSibling: () -> EventTemplate = { + HlsKind1SiblingBuilder.build( + title = request.title, + description = request.description, + masterUrl = masterUrl, + masterSha256 = masterUpload.sha256, + masterDimension = built.masterDimension, + posterUrl = posterResult?.url, + blurhashValue = posterResult?.blurhash, + thumbhashValue = posterResult?.thumbhash, + ) + } + val eventId = signAndPublish(built.template, buildSibling) _state.value = HlsPublishState.Success( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt index cf180e8a7..86e66701e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt @@ -88,7 +88,7 @@ fun createProductionHlsPublishOrchestrator( } } }, - signAndPublish = { template -> + signAndPublish = { template, buildSibling -> val inner = when (template) { is HlsVideoEventTemplate.Horizontal -> template.template @@ -96,6 +96,21 @@ fun createProductionHlsPublishOrchestrator( } val signed = account.signer.sign(inner) account.sendAutomatic(signed) + + // Auto-publish the kind:1 sibling note so receivers that don't speak NIP-71 still + // see a rich preview (poster + dim + blurhash/thumbhash) and can hop to the + // addressable form via the imeta + `a` tag. Publishing the sibling must not throw + // out of the orchestrator on signer failure; the NIP-71 event is already broadcast + // and surfacing as a partial success is more useful than a hard fail. + val siblingTemplate = buildSibling() + try { + val signedSibling = account.signer.sign(siblingTemplate) + account.sendAutomatic(signedSibling) + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (e: Throwable) { + Log.w(TAG) { "kind:1 sibling sign/publish failed: ${e.message}" } + } signed.id }, uploadPoster = { uploader -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt index 38cc496ac..36efa974d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt @@ -83,7 +83,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.collections.immutable.toImmutableList @@ -323,31 +322,6 @@ private fun FormFields(vm: NewHlsVideoViewModel) { ) } - Spacer(Modifier.height(8.dp)) - - // Draft-a-note-after-upload toggle — opens the existing short-note composer prefilled - // with the title, description and master playlist URL; the user edits and posts. - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.hls_draft_note_after_upload), - style = MaterialTheme.typography.bodyLarge, - ) - Text( - text = stringResource(R.string.hls_draft_note_after_upload_explainer), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = vm.draftNoteAfterUpload, - onCheckedChange = { vm.draftNoteAfterUpload = it }, - ) - } - Spacer(Modifier.height(16.dp)) // Server picker — reads the user's configured Blossom servers from the account @@ -686,54 +660,18 @@ private fun SuccessBody( ) Spacer(Modifier.height(16.dp)) - if (vm.draftNoteAfterUpload) { - Button( - onClick = { - val draft = buildDraftNoteText(vm.title, vm.description, state.masterUrl) - vm.reset() - // Pop the HLS publish screen off the back stack as we open the composer, - // so that after the user posts (or backs out of) the draft they land on - // the screen they came from, not back on the HLS publish flow. - nav.popUpTo(Route.NewShortNote(message = draft), Route.NewHlsVideo::class) - }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.hls_draft_note_button)) - } - Spacer(Modifier.height(8.dp)) - OutlinedButton( - onClick = { - vm.reset() - nav.popBack() - }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.hls_done)) - } - } else { - Button( - onClick = { - vm.reset() - nav.popBack() - }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.hls_done)) - } + Button( + onClick = { + vm.reset() + nav.popBack() + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.hls_done)) } } } -private fun buildDraftNoteText( - title: String, - description: String, - masterUrl: String, -): String = - listOf(title, description, masterUrl) - .map { it.trim() } - .filter { it.isNotEmpty() } - .joinToString("\n\n") - @Composable private fun FailureBody( vm: NewHlsVideoViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt index eaccaa3a4..f5115a3fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt @@ -64,7 +64,6 @@ open class NewHlsVideoViewModel : ViewModel() { var sensitiveContent by mutableStateOf(false) var contentWarningReason by mutableStateOf("") var useH265 by mutableStateOf(true) - var draftNoteAfterUpload by mutableStateOf(true) var selectedServer by mutableStateOf(null) var selectedRenditionLabels by mutableStateOf( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 009f3e25c..c5d072167 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2586,9 +2586,6 @@ View note Done Try again - Draft note after upload - Open the note composer pre-filled with the title, description and video link so you can tweak it before posting. - Draft note Pack Actions List Actions diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt index 943d7ac8e..fa5d58bb0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt @@ -161,7 +161,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { tpl -> + signAndPublish = { tpl, _ -> publishedTemplates += tpl "signed-event-id" }, @@ -221,7 +221,7 @@ class HlsPublishOrchestratorTest { runUpload = capturingRunUpload, buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { + signAndPublish = { _, _ -> capturedDuringPublish += orchestrator.state.value "event-id" }, @@ -247,7 +247,7 @@ class HlsPublishOrchestratorTest { runUpload = { _, _, _ -> throw RuntimeException("decode failed") }, buildUploader = { CannedUploader() }, uploadMaster = { _, _ -> MediaUploadResult(url = "never") }, - signAndPublish = { "never" }, + signAndPublish = { _, _ -> "never" }, ) runBlocking { orchestrator.publish(newRequest()) } @@ -267,7 +267,7 @@ class HlsPublishOrchestratorTest { HlsBlobUploader { _, _, _ -> throw RuntimeException("server 500") } }, uploadMaster = { _, _ -> MediaUploadResult(url = "never") }, - signAndPublish = { "never" }, + signAndPublish = { _, _ -> "never" }, ) runBlocking { orchestrator.publish(newRequest()) } @@ -286,7 +286,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = { _, _ -> throw RuntimeException("master upload failed") }, - signAndPublish = { "never" }, + signAndPublish = { _, _ -> "never" }, ) runBlocking { orchestrator.publish(newRequest()) } @@ -305,7 +305,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { throw RuntimeException("relay rejected") }, + signAndPublish = { _, _ -> throw RuntimeException("relay rejected") }, ) runBlocking { orchestrator.publish(newRequest()) } @@ -325,7 +325,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { tpl -> + signAndPublish = { tpl, _ -> captured += tpl "event-id" }, @@ -363,7 +363,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(portrait), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { tpl -> + signAndPublish = { tpl, _ -> captured += tpl "event-id" }, @@ -384,7 +384,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { tpl -> + signAndPublish = { tpl, _ -> captured += tpl "event-id" }, @@ -412,7 +412,7 @@ class HlsPublishOrchestratorTest { runUpload = fakeRunUpload(), buildUploader = { canned }, uploadMaster = fakeUploadMaster(canned), - signAndPublish = { tpl -> + signAndPublish = { tpl, _ -> captured += tpl "event-id" }, @@ -440,7 +440,7 @@ class HlsPublishOrchestratorTest { runUpload = { _, _, _ -> throw RuntimeException("boom") }, buildUploader = { CannedUploader() }, uploadMaster = { _, _ -> MediaUploadResult(url = "never") }, - signAndPublish = { "never" }, + signAndPublish = { _, _ -> "never" }, ) runBlocking { orchestrator.publish(newRequest()) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt index f21f6b63b..84e6c042a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt @@ -120,8 +120,8 @@ class HlsVideoEventBuilderTest { fun landscapeRenditionsBuildHorizontalTemplateKind34235() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - assertTrue("expected Horizontal template", result is HlsVideoEventTemplate.Horizontal) - val template = (result as HlsVideoEventTemplate.Horizontal).template + assertTrue("expected Horizontal template", result.template is HlsVideoEventTemplate.Horizontal) + val template = (result.template as HlsVideoEventTemplate.Horizontal).template assertEquals(VideoHorizontalEvent.KIND, template.kind) assertEquals("A cool video", template.content) } @@ -130,15 +130,15 @@ class HlsVideoEventBuilderTest { fun portraitRenditionsBuildVerticalTemplateKind34236() { val result = HlsVideoEventBuilder.build(input(portraitRenditions)) - assertTrue("expected Vertical template", result is HlsVideoEventTemplate.Vertical) - val template = (result as HlsVideoEventTemplate.Vertical).template + assertTrue("expected Vertical template", result.template is HlsVideoEventTemplate.Vertical) + val template = (result.template as HlsVideoEventTemplate.Vertical).template assertEquals(VideoVerticalEvent.KIND, template.kind) } @Test fun horizontalTemplateHasTitleAndDTag() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val title = tags.findTag("title") assertNotNull(title) @@ -152,7 +152,7 @@ class HlsVideoEventBuilderTest { @Test fun templateContainsOneImetaForMasterAndOnePerRendition() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val imetas = tags.findAllTags("imeta") // 1 master + 2 renditions @@ -181,7 +181,7 @@ class HlsVideoEventBuilderTest { HlsVideoEventBuilder.build( input(landscapeRenditions, duration = 123), ) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val duration = tags.findTag("duration") assertNotNull(duration) @@ -191,7 +191,7 @@ class HlsVideoEventBuilderTest { @Test fun noDurationTagWhenNotProvided() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags assertNull(tags.findTag("duration")) } @@ -201,7 +201,7 @@ class HlsVideoEventBuilderTest { HlsVideoEventBuilder.build( input(landscapeRenditions, contentWarning = "NSFW"), ) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val warning = tags.findTag("content-warning") assertNotNull(warning) @@ -211,14 +211,14 @@ class HlsVideoEventBuilderTest { @Test fun noContentWarningTagWhenNull() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags assertNull(tags.findTag("content-warning")) } @Test fun horizontalTemplateCarriesAutoGeneratedAltTag() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val alt = tags.findTag("alt") assertNotNull(alt) assertEquals(VideoHorizontalEvent.ALT_DESCRIPTION, alt!![1]) @@ -227,7 +227,7 @@ class HlsVideoEventBuilderTest { @Test fun verticalTemplateCarriesVerticalAltTag() { val result = HlsVideoEventBuilder.build(input(portraitRenditions)) - val tags = (result as HlsVideoEventTemplate.Vertical).template.tags + val tags = (result.template as HlsVideoEventTemplate.Vertical).template.tags val alt = tags.findTag("alt") assertNotNull(alt) assertEquals(VideoVerticalEvent.ALT_DESCRIPTION, alt!![1]) @@ -239,7 +239,7 @@ class HlsVideoEventBuilderTest { HlsVideoEventBuilder.build( input(landscapeRenditions, posterUrl = "https://cdn.test/poster.jpg"), ) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val imetas = tags.findAllTags("imeta") // 1 master + 2 renditions, each must carry image @@ -253,7 +253,7 @@ class HlsVideoEventBuilderTest { @Test fun noPosterUrlMeansNoImagePropertyOnImetas() { val result = HlsVideoEventBuilder.build(input(landscapeRenditions)) - val tags = (result as HlsVideoEventTemplate.Horizontal).template.tags + val tags = (result.template as HlsVideoEventTemplate.Horizontal).template.tags val imetas = tags.findAllTags("imeta") imetas.forEach { imeta ->