mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-25 09:50:19 +00:00
feat: hls video support (#6542)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
80dc1f5aed
commit
4eee28641f
@ -1586,6 +1586,9 @@ dependencies:
|
|||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ~11.8.0
|
specifier: ~11.8.0
|
||||||
version: 11.8.0
|
version: 11.8.0
|
||||||
|
hls.js:
|
||||||
|
specifier: ^1.5.15
|
||||||
|
version: 1.5.15
|
||||||
html-to-text:
|
html-to-text:
|
||||||
specifier: ^9.0.3
|
specifier: ^9.0.3
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
@ -15598,6 +15601,10 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/hls.js@1.5.15:
|
||||||
|
resolution: {integrity: sha512-6cD7xN6bycBHaXz2WyPIaHn/iXFizE5au2yvY5q9aC4wfihxAr16C9fUy4nxh2a3wOw0fEgLRa9dN6wsYjlpNg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/hogan.js@3.0.2:
|
/hogan.js@3.0.2:
|
||||||
resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==}
|
resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -27291,7 +27298,7 @@ packages:
|
|||||||
dev: false
|
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):
|
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
|
id: file:projects/lead-resources.tgz
|
||||||
name: '@rush-temp/lead-resources'
|
name: '@rush-temp/lead-resources'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -30482,6 +30489,7 @@ packages:
|
|||||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
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)
|
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||||
fast-equals: 5.0.1
|
fast-equals: 5.0.1
|
||||||
|
hls.js: 1.5.15
|
||||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||||
png-chunks-extract: 1.0.0
|
png-chunks-extract: 1.0.0
|
||||||
prettier: 3.2.5
|
prettier: 3.2.5
|
||||||
@ -34795,7 +34803,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/tool.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
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
|
id: file:projects/tool.tgz
|
||||||
name: '@rush-temp/tool'
|
name: '@rush-temp/tool'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -35311,7 +35319,7 @@ packages:
|
|||||||
dev: false
|
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):
|
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
|
id: file:projects/view-resources.tgz
|
||||||
name: '@rush-temp/view-resources'
|
name: '@rush-temp/view-resources'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -35326,6 +35334,7 @@ packages:
|
|||||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
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)
|
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||||
fast-equals: 5.0.1
|
fast-equals: 5.0.1
|
||||||
|
hls.js: 1.5.15
|
||||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||||
prettier: 3.2.5
|
prettier: 3.2.5
|
||||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||||
|
@ -6,27 +6,56 @@ import { getFileUrl, getCurrentWorkspaceId } from './file'
|
|||||||
import presentation from './plugin'
|
import presentation from './plugin'
|
||||||
|
|
||||||
export interface PreviewConfig {
|
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.
|
* 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 {
|
export function parsePreviewConfig (config?: string): PreviewConfig | undefined {
|
||||||
if (config === undefined) {
|
if (config === undefined) {
|
||||||
return
|
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 {
|
export function getPreviewConfig (): PreviewConfig {
|
||||||
return (
|
return (
|
||||||
(getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? {
|
(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 ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceId()))
|
let url = cfg.image.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceId()))
|
||||||
const downloadUrl = getFileUrl(blob)
|
const downloadUrl = getFileUrl(blob)
|
||||||
|
|
||||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
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 {
|
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
|
||||||
return blobToSrcSet(getPreviewConfig(), _blob, width)
|
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 {}
|
||||||
|
}
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"@hcengineering/text-editor-resources": "^0.6.0",
|
"@hcengineering/text-editor-resources": "^0.6.0",
|
||||||
"@hcengineering/analytics": "^0.6.0",
|
"@hcengineering/analytics": "^0.6.0",
|
||||||
"@hcengineering/query": "^0.6.12",
|
"@hcengineering/query": "^0.6.12",
|
||||||
"fast-equals": "^5.0.1"
|
"fast-equals": "^5.0.1",
|
||||||
|
"hls.js": "^1.5.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,20 +14,46 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
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 value: Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
export let metadata: BlobMetadata | undefined
|
export let metadata: BlobMetadata | undefined
|
||||||
export let fit: boolean = false
|
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
|
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
|
||||||
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
|
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
|
||||||
|
$: void fetchVideoMeta(value, name)
|
||||||
$: src = getFileUrl(value, name)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<video style:max-width={fit ? '100%' : maxWidth} style:max-height={fit ? '100%' : maxHeight} controls preload={'auto'}>
|
<video
|
||||||
<source {src} />
|
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} />
|
<track kind="captions" label={name} />
|
||||||
</video>
|
</video>
|
||||||
|
@ -23,15 +23,12 @@ Front service is suited to deliver application bundles and resource assets, it a
|
|||||||
|
|
||||||
PREVIEW_CONFIG env variable format.
|
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.
|
* mediaType - a type of media, image or video.
|
||||||
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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
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
|
## Variables
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user