UNPKG

sanity-plugin-mux-input

Version:

An input component that integrates Sanity Studio with Mux video encoding/hosting service.

266 lines (250 loc) 7.79 kB
import {uuid as generateUuid} from '@sanity/uuid' import {concat, defer, from, type Observable, of, throwError} from 'rxjs' import {catchError, mergeMap, mergeMapTo, switchMap} from 'rxjs/operators' import type {SanityClient} from 'sanity' import {createUpChunkObservable} from '../clients/upChunkObservable' import type {MuxAsset, MuxNewAssetSettings} from '../util/types' import {getAsset} from './assets' import {testSecretsObservable} from './secrets' export function cancelUpload(client: SanityClient, uuid: string) { return client.observable.request({ url: `/addons/mux/uploads/${client.config().dataset}/${uuid}`, withCredentials: true, method: 'DELETE', }) } export function uploadUrl({ url, settings, client, }: { url: string settings: MuxNewAssetSettings client: SanityClient }) { return testUrl(url).pipe( switchMap((validUrl) => { return concat( of({type: 'url' as const, url: validUrl}), testSecretsObservable(client).pipe( switchMap((json) => { if (!json || !json.status) { return throwError(new Error('Invalid credentials')) } const uuid = generateUuid() const muxBody = settings if (!muxBody.input) muxBody.input = [{type: 'video'}] muxBody.input[0].url = validUrl const query = { muxBody: JSON.stringify(muxBody), filename: validUrl.split('/').slice(-1)[0], } const dataset = client.config().dataset return defer(() => client.observable.request({ url: `/addons/mux/assets/${dataset}`, withCredentials: true, method: 'POST', headers: { 'MUX-Proxy-UUID': uuid, 'Content-Type': 'application/json', }, query, }) ).pipe( mergeMap((result) => { const asset = (result && result.results && result.results[0] && result.results[0].document) || null if (!asset) { return throwError(new Error('No asset document returned')) } return of({type: 'success' as const, id: uuid, asset}) }) ) }) ) ) }) ) } export function uploadFile({ settings, client, file, }: { settings: MuxNewAssetSettings client: SanityClient file: File }) { return testFile(file).pipe( switchMap((fileOptions) => { return concat( of({type: 'file' as const, file: fileOptions}), testSecretsObservable(client).pipe( switchMap((json) => { if (!json || !json.status) { return throwError(() => new Error('Invalid credentials')) } const uuid = generateUuid() const body = settings return concat( of({type: 'uuid' as const, uuid}), defer(() => client.observable.request<{ sanityAssetId: string upload: { cors_origin: string id: string new_asset_settings: MuxNewAssetSettings status: 'waiting' timeout: number url: string } }>({ url: `/addons/mux/uploads/${client.config().dataset}`, withCredentials: true, method: 'POST', headers: { 'MUX-Proxy-UUID': uuid, 'Content-Type': 'application/json', }, body, }) ).pipe( mergeMap((result) => { return createUpChunkObservable(uuid, result.upload.url, file).pipe( // eslint-disable-next-line no-warning-comments // @TODO type the observable events // eslint-disable-next-line max-nested-callbacks mergeMap((event) => { if (event.type !== 'success') { return of(event) } return from(updateAssetDocumentFromUpload(client, uuid)).pipe( // eslint-disable-next-line max-nested-callbacks mergeMap((doc) => of({...event, asset: doc})) ) }), // eslint-disable-next-line max-nested-callbacks catchError((err) => { // Delete asset document return cancelUpload(client, uuid).pipe(mergeMapTo(throwError(err))) }) ) }) ) ) }) ) ) }) ) } type UploadResponse = { data: { asset_id: string cors_origin: string id: string new_asset_settings: { mp4_support: 'standard' | 'none' passthrough: string playback_policies: ['public' | 'signed'] } status: string timeout: number } } export function getUpload(client: SanityClient, assetId: string) { const {dataset} = client.config() return client.request<UploadResponse>({ url: `/addons/mux/uploads/${dataset}/${assetId}`, withCredentials: true, method: 'GET', }) } function pollUpload(client: SanityClient, uuid: string): Promise<UploadResponse> { const maxTries = 10 let pollInterval: number let tries = 0 let assetId: string let upload: UploadResponse return new Promise((resolve, reject) => { pollInterval = (setInterval as typeof window.setInterval)(async () => { try { upload = await getUpload(client, uuid) } catch (err) { reject(err) return } assetId = upload && upload.data && upload.data.asset_id if (assetId) { clearInterval(pollInterval) resolve(upload) } if (tries > maxTries) { clearInterval(pollInterval) reject(new Error('Upload did not finish')) } tries++ }, 2000) }) } async function updateAssetDocumentFromUpload(client: SanityClient, uuid: string) { let upload: UploadResponse let asset: {data: MuxAsset} try { upload = await pollUpload(client, uuid) } catch (err) { return Promise.reject(err) } try { asset = await getAsset(client, upload.data.asset_id) } catch (err) { return Promise.reject(err) } const doc = { _id: uuid, _type: 'mux.videoAsset', status: asset.data.status, data: asset.data, assetId: asset.data.id, playbackId: asset.data.playback_ids[0].id, uploadId: upload.data.id, } return client.createOrReplace(doc).then(() => { return doc }) } export function testFile(file: File) { if (typeof window !== 'undefined' && file instanceof window.File) { const fileOptions = optionsFromFile({}, file) return of(fileOptions) } return throwError(new Error('Invalid file')) } export function testUrl(url: string): Observable<string> { const error = new Error('Invalid URL') if (typeof url !== 'string') { return throwError(error) } let parsed try { parsed = new URL(url) } catch (err) { return throwError(error) } if (parsed && !parsed.protocol.match(/http:|https:/)) { return throwError(error) } return of(url) } function optionsFromFile(opts: {preserveFilename?: boolean}, file: File) { if (typeof window === 'undefined' || !(file instanceof window.File)) { return undefined } return { name: opts.preserveFilename === false ? undefined : file.name, type: file.type, } }