feat(hls): auto-publish kind:1 sibling note with rich imeta
This commit is contained in:
+91
@@ -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<TextNoteEvent> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-20
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -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<MediaUploadResult>,
|
||||
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<TextNoteEvent>,
|
||||
) -> 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<TextNoteEvent> = {
|
||||
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(
|
||||
|
||||
+16
-1
@@ -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 ->
|
||||
|
||||
+8
-70
@@ -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,
|
||||
|
||||
-1
@@ -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<ServerName?>(null)
|
||||
|
||||
var selectedRenditionLabels by mutableStateOf(
|
||||
|
||||
@@ -2586,9 +2586,6 @@
|
||||
<string name="hls_view_note">View note</string>
|
||||
<string name="hls_done">Done</string>
|
||||
<string name="hls_try_again">Try again</string>
|
||||
<string name="hls_draft_note_after_upload">Draft note after upload</string>
|
||||
<string name="hls_draft_note_after_upload_explainer">Open the note composer pre-filled with the title, description and video link so you can tweak it before posting.</string>
|
||||
<string name="hls_draft_note_button">Draft note</string>
|
||||
|
||||
<string name="pack_actions_dialog_title">Pack Actions</string>
|
||||
<string name="list_actions_dialog_title">List Actions</string>
|
||||
|
||||
+11
-11
@@ -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()) }
|
||||
|
||||
+14
-14
@@ -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 <posterUrl>
|
||||
@@ -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 ->
|
||||
|
||||
Reference in New Issue
Block a user