diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 6 | ||||
-rw-r--r-- | src/components/VideoDownloadScreen.native.tsx | 4 | ||||
-rw-r--r-- | src/components/VideoDownloadScreen.tsx | 215 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 1 | ||||
-rw-r--r-- | src/routes.ts | 1 | ||||
-rw-r--r-- | src/view/screens/Storybook/index.tsx | 46 |
6 files changed, 1 insertions, 272 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 0d151427f..79856879c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -50,7 +50,6 @@ import { StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' -import {VideoDownloadScreen} from '#/components/VideoDownloadScreen' import {Referrer} from '../modules/expo-bluesky-swiss-army' import {init as initAnalytics} from './lib/analytics/analytics' import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' @@ -365,11 +364,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => Wizard} options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> - <Stack.Screen - name="VideoDownload" - getComponent={() => VideoDownloadScreen} - options={{title: title(msg`Download video`)}} - /> </> ) } diff --git a/src/components/VideoDownloadScreen.native.tsx b/src/components/VideoDownloadScreen.native.tsx deleted file mode 100644 index a1f6466fd..000000000 --- a/src/components/VideoDownloadScreen.native.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export function VideoDownloadScreen() { - // @TODO redirect - return null -} diff --git a/src/components/VideoDownloadScreen.tsx b/src/components/VideoDownloadScreen.tsx deleted file mode 100644 index 3169d265d..000000000 --- a/src/components/VideoDownloadScreen.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react' -import {parse} from 'hls-parser' -import {MasterPlaylist, MediaPlaylist, Variant} from 'hls-parser/types' - -interface PostMessageData { - action: 'progress' | 'error' - messageStr?: string - messageFloat?: number -} - -function postMessage(data: PostMessageData) { - // @ts-expect-error safari webview only - if (window?.webkit) { - // @ts-expect-error safari webview only - window.webkit.messageHandlers.onMessage.postMessage(JSON.stringify(data)) - // @ts-expect-error android webview only - } else if (AndroidInterface) { - // @ts-expect-error android webview only - AndroidInterface.onMessage(JSON.stringify(data)) - } -} - -function createSegementUrl(originalUrl: string, newFile: string) { - const parts = originalUrl.split('/') - parts[parts.length - 1] = newFile - return parts.join('/') -} - -export function VideoDownloadScreen() { - const ffmpegRef = React.useRef<any>(null) - const fetchFileRef = React.useRef<any>(null) - - const [dataUrl, setDataUrl] = React.useState<any>(null) - - const load = React.useCallback(async () => { - const ffmpegLib = await import('@ffmpeg/ffmpeg') - const ffmpeg = new ffmpegLib.FFmpeg() - ffmpegRef.current = ffmpeg - - const ffmpegUtilLib = await import('@ffmpeg/util') - fetchFileRef.current = ffmpegUtilLib.fetchFile - - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm' - - await ffmpeg.load({ - coreURL: await ffmpegUtilLib.toBlobURL( - `${baseURL}/ffmpeg-core.js`, - 'text/javascript', - ), - wasmURL: await ffmpegUtilLib.toBlobURL( - `${baseURL}/ffmpeg-core.wasm`, - 'application/wasm', - ), - }) - }, []) - - const createMp4 = React.useCallback(async (videoUrl: string) => { - // Get the master playlist and find the best variant - const masterPlaylistRes = await fetch(videoUrl) - const masterPlaylistText = await masterPlaylistRes.text() - const masterPlaylist = parse(masterPlaylistText) as MasterPlaylist - - // If URL given is not a master playlist, we probably cannot handle this. - if (!masterPlaylist.isMasterPlaylist) { - postMessage({ - action: 'error', - messageStr: 'A master playlist was not found in the provided playlist.', - }) - return - } - - // Figure out what the best quality is. These should generally be in order, but we'll check them all just in case - let bestVariant: Variant | undefined - for (const variant of masterPlaylist.variants) { - if (!bestVariant || variant.bandwidth > bestVariant.bandwidth) { - bestVariant = variant - } - } - - // Should only happen if there was no variants at all given to us. Mostly for types. - if (!bestVariant) { - postMessage({ - action: 'error', - messageStr: 'No variants were found in the provided master playlist.', - }) - return - } - - const urlParts = videoUrl.split('/') - urlParts[urlParts.length - 1] = bestVariant?.uri - const bestVariantUrl = urlParts.join('/') - - // Download and parse m3u8 - const hlsFileRes = await fetch(bestVariantUrl) - const hlsPlainText = await hlsFileRes.text() - const playlist = parse(hlsPlainText) as MediaPlaylist - - // This one shouldn't be a master playlist - again just for types really - if (playlist.isMasterPlaylist) { - postMessage({ - action: 'error', - messageStr: 'An unknown error has occurred.', - }) - return - } - - const ffmpeg = ffmpegRef.current - - // Get the correctly ordered file names. We need to remove the tracking info from the end of the file name - const segments = playlist.segments.map(segment => { - return segment.uri.split('?')[0] - }) - - // Download each segment - let error: string | null = null - let completed = 0 - await Promise.all( - playlist.segments.map(async segment => { - const uri = createSegementUrl(bestVariantUrl, segment.uri) - const filename = segment.uri.split('?')[0] - - const res = await fetch(uri) - if (!res.ok) { - error = 'Failed to download playlist segment.' - } - - const blob = await res.blob() - try { - await ffmpeg.writeFile(filename, await fetchFileRef.current(blob)) - } catch (e: unknown) { - error = 'Failed to write file.' - } finally { - completed++ - const progress = completed / playlist.segments.length - postMessage({ - action: 'progress', - messageFloat: progress, - }) - } - }), - ) - - // Do something if there was an error - if (error) { - postMessage({ - action: 'error', - messageStr: error, - }) - return - } - - // Put the segments together - await ffmpeg.exec([ - '-i', - `concat:${segments.join('|')}`, - '-c:v', - 'copy', - 'output.mp4', - ]) - - const fileData = await ffmpeg.readFile('output.mp4') - const blob = new Blob([fileData.buffer], {type: 'video/mp4'}) - const dataUrl = await new Promise<string | null>(resolve => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result as string) - reader.onerror = () => resolve(null) - reader.readAsDataURL(blob) - }) - return dataUrl - }, []) - - const download = React.useCallback( - async (videoUrl: string) => { - await load() - const mp4Res = await createMp4(videoUrl) - - if (!mp4Res) { - postMessage({ - action: 'error', - messageStr: 'An error occurred while creating the MP4.', - }) - return - } - - setDataUrl(mp4Res) - }, - [createMp4, load], - ) - - React.useEffect(() => { - const url = new URL(window.location.href) - const videoUrl = url.searchParams.get('videoUrl') - - if (!videoUrl) { - postMessage({action: 'error', messageStr: 'No video URL provided'}) - } else { - setDataUrl(null) - download(videoUrl) - } - }, [download]) - - if (!dataUrl) return null - - return ( - <div> - <a - href={dataUrl} - ref={el => { - el?.click() - }} - download="video.mp4" - /> - </div> - ) -} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 77e7266a4..0cc83b475 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -50,7 +50,6 @@ export type CommonNavigatorParams = { StarterPackShort: {code: string} StarterPackWizard: undefined StarterPackEdit: {rkey?: string} - VideoDownload: undefined } export type BottomTabNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index bda2d98e4..c9e23e08c 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -48,5 +48,4 @@ export const router = new Router({ StarterPack: '/starter-pack/:name/:rkey', StarterPackShort: '/starter-pack-short/:code', StarterPackWizard: '/starter-pack/create', - VideoDownload: '/video-download', }) diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index c6da63314..71dbe8839 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -1,17 +1,12 @@ import React from 'react' import {ScrollView, View} from 'react-native' -import {deleteAsync} from 'expo-file-system' -import {saveToLibraryAsync} from 'expo-media-library' import {useSetThemePrefs} from '#/state/shell' -import {useVideoLibraryPermission} from 'lib/hooks/usePermissions' -import {isIOS, isWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' import {CenteredView} from '#/view/com/util/Views' -import * as Toast from 'view/com/util/Toast' import {ListContained} from 'view/screens/Storybook/ListContained' import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' -import {HLSDownloadView} from '../../../../modules/expo-bluesky-swiss-army' import {Breakpoints} from './Breakpoints' import {Buttons} from './Buttons' import {Dialogs} from './Dialogs' @@ -38,49 +33,10 @@ function StorybookInner() { const t = useTheme() const {setColorMode, setDarkTheme} = useSetThemePrefs() const [showContainedList, setShowContainedList] = React.useState(false) - const hlsDownloadRef = React.useRef<HLSDownloadView>(null) - - const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() return ( <CenteredView style={[t.atoms.bg]}> <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 100}]}> - <HLSDownloadView - ref={hlsDownloadRef} - downloaderUrl={ - isIOS - ? 'http://localhost:19006/video-download' - : 'http://10.0.2.2:19006/video-download' - } - onSuccess={async e => { - const uri = e.nativeEvent.uri - const permsRes = await requestVideoAccessIfNeeded() - if (!permsRes) return - - await saveToLibraryAsync(uri) - try { - deleteAsync(uri) - } catch (err) { - console.error('Failed to delete file', err) - } - Toast.show('Video saved to library') - }} - onStart={() => console.log('Download is starting')} - onError={e => console.log(e.nativeEvent.message)} - onProgress={e => console.log(e.nativeEvent.progress)} - /> - <Button - variant="solid" - color="primary" - size="small" - onPress={async () => { - hlsDownloadRef.current?.startDownloadAsync( - 'https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?download=true', - ) - }} - label="Video download test"> - <ButtonText>Video download test</ButtonText> - </Button> {!showContainedList ? ( <> <View style={[a.flex_row, a.align_start, a.gap_md]}> |