feat: hls video support (#6542)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-09-13 16:22:21 +07:00 committed by GitHub
parent 80dc1f5aed
commit 4eee28641f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 106 additions and 22 deletions

View File

@ -1586,6 +1586,9 @@ dependencies:
highlight.js:
specifier: ~11.8.0
version: 11.8.0
hls.js:
specifier: ^1.5.15
version: 1.5.15
html-to-text:
specifier: ^9.0.3
version: 9.0.5
@ -15598,6 +15601,10 @@ packages:
requiresBuild: true
dev: false
/hls.js@1.5.15:
resolution: {integrity: sha512-6cD7xN6bycBHaXz2WyPIaHn/iXFizE5au2yvY5q9aC4wfihxAr16C9fUy4nxh2a3wOw0fEgLRa9dN6wsYjlpNg==}
dev: false
/hogan.js@3.0.2:
resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==}
hasBin: true
@ -27291,7 +27298,7 @@ packages:
dev: false
file:projects/lead-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-TJVh5S1o+GvRWeeNWwXveGOpMtsQTR0n5RMGjK3kXsuAUYCQRPzgMh3noJxe2n4vvijK8IUNjAR9+AjeSPo5kw==, tarball: file:projects/lead-resources.tgz}
resolution: {integrity: sha512-xg8Fq55+BYSO+pwIkFTJFDJGPu1CWGB8CiZ64+J2jqzbAHkRaiOCP0u3R4lOw/z6k1tnqhL0m2bvV9pCUCYTHA==, tarball: file:projects/lead-resources.tgz}
id: file:projects/lead-resources.tgz
name: '@rush-temp/lead-resources'
version: 0.0.0
@ -30482,6 +30489,7 @@ packages:
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
fast-equals: 5.0.1
hls.js: 1.5.15
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
png-chunks-extract: 1.0.0
prettier: 3.2.5
@ -34795,7 +34803,7 @@ packages:
dev: false
file:projects/tool.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-LwQbmBaSOZ5IKwCHz2mULcIuEr9rZ2b/7tqUGICHCawUzexUlQVxv2Yt0oFf2aZu83Sittt7dZwnN3sXHX9t9g==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-sZH5yB7zg/kTpuIhLSqPYh0wFgw4aOpsriMq4wad8ZHRzlHASseyJAbEylIP8ltfPbFFN4Yy1nXaUOXS49anHg==, tarball: file:projects/tool.tgz}
id: file:projects/tool.tgz
name: '@rush-temp/tool'
version: 0.0.0
@ -35311,7 +35319,7 @@ packages:
dev: false
file:projects/view-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-l/K7osn3HZ3KIFeCyBe+rxQGUxvLvM+35if2HgylqgbWtD10Gk/rR+vuW1L54o8hT4ADMkbhvBW7VHE19isd+w==, tarball: file:projects/view-resources.tgz}
resolution: {integrity: sha512-g6op8hiY1zLsms7Sab4cAs29Ucbk6r20mx9hkZrhxn70uPW/VCLS+JW67cfWf85SyMwMloWuvY6ujfQfwNuScw==, tarball: file:projects/view-resources.tgz}
id: file:projects/view-resources.tgz
name: '@rush-temp/view-resources'
version: 0.0.0
@ -35326,6 +35334,7 @@ packages:
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
fast-equals: 5.0.1
hls.js: 1.5.15
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)

View File

@ -6,27 +6,56 @@ import { getFileUrl, getCurrentWorkspaceId } from './file'
import presentation from './plugin'
export interface PreviewConfig {
previewUrl: string
image: string
video: string
}
const defaultPreview = (): string => `/files/${getCurrentWorkspaceId()}?file=:blobId&size=:size`
export interface VideoMeta {
status: 'ready' | 'error' | 'inprogress' | 'queued' | 'downloading' | 'pendingupload'
thumbnail: string
hls: string
}
const defaultImagePreview = (): string => `/files/${getCurrentWorkspaceId()}?file=:blobId&size=:size`
/**
*
* PREVIEW_CONFIG env variable format.
* previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
* - image - an Url with :workspace, :blobId, :downloadFile, :size placeholders.
* - video - an Url with :workspace, :blobId placeholders.
*/
export function parsePreviewConfig (config?: string): PreviewConfig | undefined {
if (config === undefined) {
return
}
return { previewUrl: config }
const previewConfig = { image: defaultImagePreview(), video: '' }
const configs = config.split(';')
for (const c of configs) {
if (c.includes('|')) {
const [key, value] = c.split('|')
if (key === 'image') {
previewConfig.image = value
} else if (key === 'video') {
previewConfig.video = value
} else {
throw new Error(`Unknown preview config key: ${key}`)
}
} else {
// fallback to image-only config for compatibility
previewConfig.image = c
}
}
return Object.freeze(previewConfig)
}
export function getPreviewConfig (): PreviewConfig {
return (
(getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? {
previewUrl: defaultPreview()
image: defaultImagePreview(),
video: ''
}
)
}
@ -58,7 +87,7 @@ function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | unde
return ''
}
let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceId()))
let url = cfg.image.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceId()))
const downloadUrl = getFileUrl(blob)
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
@ -89,3 +118,25 @@ function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | unde
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width)
}
/**
* @public
*/
export async function getVideoMeta (file: string, filename?: string): Promise<VideoMeta | undefined> {
const cfg = getPreviewConfig()
const url = cfg.video
.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceId()))
.replaceAll(':blobId', encodeURIComponent(file))
if (url === '') {
return undefined
}
try {
const response = await fetch(url)
if (response.ok) {
return (await response.json()) as VideoMeta
}
} catch {}
}

View File

@ -57,6 +57,7 @@
"@hcengineering/text-editor-resources": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/query": "^0.6.12",
"fast-equals": "^5.0.1"
"fast-equals": "^5.0.1",
"hls.js": "^1.5.15"
}
}

View File

@ -14,20 +14,46 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getFileUrl, type BlobMetadata } from '@hcengineering/presentation'
import { getFileUrl, getVideoMeta, type BlobMetadata } from '@hcengineering/presentation'
import HLS from 'hls.js'
export let value: Ref<Blob>
export let name: string
export let metadata: BlobMetadata | undefined
export let fit: boolean = false
let video: HTMLVideoElement
async function fetchVideoMeta (value: Ref<Blob>, name: string): Promise<void> {
const src = getFileUrl(value, name)
const meta = await getVideoMeta(value, name)
if (meta != null && meta.status === 'ready' && HLS.isSupported()) {
const hls = new HLS()
hls.loadSource(meta.hls)
hls.attachMedia(video)
video.poster = meta.thumbnail
} else {
video.src = src
}
}
$: aspectRatio =
metadata?.originalWidth && metadata?.originalHeight
? `${metadata.originalWidth} / ${metadata.originalHeight}`
: '16 / 9'
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
$: src = getFileUrl(value, name)
$: void fetchVideoMeta(value, name)
</script>
<video style:max-width={fit ? '100%' : maxWidth} style:max-height={fit ? '100%' : maxHeight} controls preload={'auto'}>
<source {src} />
<video
bind:this={video}
width="100%"
style:aspect-ratio={aspectRatio}
style:max-width={fit ? '100%' : maxWidth}
style:max-height={fit ? '100%' : maxHeight}
controls
preload={'auto'}
>
<track kind="captions" label={name} />
</video>

View File

@ -23,15 +23,12 @@ Front service is suited to deliver application bundles and resource assets, it a
PREVIEW_CONFIG env variable format.
A `;` separated list of triples, providerName|previewUrl|supportedFormats.
A `;` separated list of pairs, mediaType|previewUrl.
- providerName - a provider name should be same as in Storage configuration.
It coult be empty and it will match by content types.
- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
- supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns.
* mediaType - a type of media, image or video.
* previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
PREVIEW_CONFIG=https://front.hc.engineering/files/:workspace/api/preview/?width=:size&image=:downloadFile
PREVIEW_CONFIG=image|https://front.hc.engineering/files/:workspace/api/preview/?width=:size&image=:downloadFile
## Variables