deer social fork for personal usage. but you might see a use idk. github mirror

Revert "[Video] Download videos" (#4945)

authored by hailey.at and committed by GitHub a5af24b5 b6e515c6

Changed files
+3 -747
bskyweb
cmd
bskyweb
static
modules
expo-bluesky-swiss-army
android
src
main
java
expo
modules
blueskyswissarmy
ios
src
src
-3
bskyweb/cmd/bskyweb/server.go
··· 256 256 e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) 257 257 e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) 258 258 259 - // video download 260 - e.GET("/video-download", server.WebGeneric) 261 - 262 259 // starter packs 263 260 e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack) 264 261 e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
-1
bskyweb/static/robots.txt
··· 7 7 # be ok. 8 8 User-Agent: * 9 9 Allow: / 10 - Disallow: /video-download
-35
modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/ExpoHLSDownloadModule.kt
··· 1 - package expo.modules.blueskyswissarmy.hlsdownload 2 - 3 - import android.net.Uri 4 - import expo.modules.kotlin.modules.Module 5 - import expo.modules.kotlin.modules.ModuleDefinition 6 - 7 - class ExpoHLSDownloadModule : Module() { 8 - override fun definition() = 9 - ModuleDefinition { 10 - Name("ExpoHLSDownload") 11 - 12 - Function("isAvailable") { 13 - return@Function true 14 - } 15 - 16 - View(HLSDownloadView::class) { 17 - Events( 18 - arrayOf( 19 - "onStart", 20 - "onError", 21 - "onProgress", 22 - "onSuccess", 23 - ), 24 - ) 25 - 26 - Prop("downloaderUrl") { view: HLSDownloadView, downloaderUrl: Uri -> 27 - view.downloaderUrl = downloaderUrl 28 - } 29 - 30 - AsyncFunction("startDownloadAsync") { view: HLSDownloadView, sourceUrl: Uri -> 31 - view.startDownload(sourceUrl) 32 - } 33 - } 34 - } 35 - }
-141
modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/HLSDownloadView.kt
··· 1 - package expo.modules.blueskyswissarmy.hlsdownload 2 - 3 - import android.annotation.SuppressLint 4 - import android.content.Context 5 - import android.net.Uri 6 - import android.util.Base64 7 - import android.util.Log 8 - import android.webkit.DownloadListener 9 - import android.webkit.JavascriptInterface 10 - import android.webkit.WebView 11 - import expo.modules.kotlin.AppContext 12 - import expo.modules.kotlin.viewevent.EventDispatcher 13 - import expo.modules.kotlin.viewevent.ViewEventCallback 14 - import expo.modules.kotlin.views.ExpoView 15 - import org.json.JSONObject 16 - import java.io.File 17 - import java.io.FileOutputStream 18 - import java.net.URI 19 - import java.util.UUID 20 - 21 - class HLSDownloadView( 22 - context: Context, 23 - appContext: AppContext, 24 - ) : ExpoView(context, appContext), 25 - DownloadListener { 26 - private val webView = WebView(context) 27 - 28 - var downloaderUrl: Uri? = null 29 - 30 - private val onStart by EventDispatcher() 31 - private val onError by EventDispatcher() 32 - private val onProgress by EventDispatcher() 33 - private val onSuccess by EventDispatcher() 34 - 35 - init { 36 - this.setupWebView() 37 - this.addView(this.webView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) 38 - } 39 - 40 - @SuppressLint("SetJavaScriptEnabled") 41 - private fun setupWebView() { 42 - val webSettings = this.webView.settings 43 - webSettings.javaScriptEnabled = true 44 - webSettings.domStorageEnabled = true 45 - 46 - webView.setDownloadListener(this) 47 - webView.addJavascriptInterface(WebAppInterface(this.onProgress, this.onError), "AndroidInterface") 48 - } 49 - 50 - override fun onDetachedFromWindow() { 51 - super.onDetachedFromWindow() 52 - this.webView.stopLoading() 53 - this.webView.clearHistory() 54 - this.webView.removeAllViews() 55 - this.webView.destroy() 56 - } 57 - 58 - fun startDownload(sourceUrl: Uri) { 59 - if (this.downloaderUrl == null) { 60 - this.onError(mapOf(ERROR_KEY to "Downloader URL is not set.")) 61 - return 62 - } 63 - 64 - val url = URI("${this.downloaderUrl}?videoUrl=$sourceUrl") 65 - this.webView.loadUrl(url.toString()) 66 - this.onStart(mapOf()) 67 - } 68 - 69 - override fun onDownloadStart( 70 - url: String?, 71 - userAgent: String?, 72 - contentDisposition: String?, 73 - mimeType: String?, 74 - contentLength: Long, 75 - ) { 76 - if (url == null) { 77 - this.onError(mapOf(ERROR_KEY to "Failed to retrieve download URL from webview.")) 78 - return 79 - } 80 - 81 - val tempDir = context.cacheDir 82 - val fileName = "${UUID.randomUUID()}.mp4" 83 - val file = File(tempDir, fileName) 84 - 85 - val base64 = url.split(",")[1] 86 - val bytes = Base64.decode(base64, Base64.DEFAULT) 87 - 88 - val fos = FileOutputStream(file) 89 - try { 90 - fos.write(bytes) 91 - } catch (e: Exception) { 92 - Log.e("FileDownload", "Error downloading file", e) 93 - this.onError(mapOf(ERROR_KEY to e.message.toString())) 94 - return 95 - } finally { 96 - fos.close() 97 - } 98 - 99 - val uri = Uri.fromFile(file) 100 - this.onSuccess(mapOf("uri" to uri.toString())) 101 - } 102 - 103 - companion object { 104 - const val ERROR_KEY = "message" 105 - } 106 - } 107 - 108 - public class WebAppInterface( 109 - val onProgress: ViewEventCallback<Map<String, Any>>, 110 - val onError: ViewEventCallback<Map<String, Any>>, 111 - ) { 112 - @JavascriptInterface 113 - public fun onMessage(message: String) { 114 - val jsonObject = JSONObject(message) 115 - val action = jsonObject.getString("action") 116 - 117 - when (action) { 118 - "error" -> { 119 - val messageStr = jsonObject.get("messageStr") 120 - if (messageStr !is String) { 121 - this.onError(mapOf(ERROR_KEY to "Failed to decode JSON post message.")) 122 - return 123 - } 124 - this.onError(mapOf(ERROR_KEY to messageStr)) 125 - } 126 - "progress" -> { 127 - val messageFloat = jsonObject.get("messageFloat") 128 - if (messageFloat !is Number) { 129 - this.onError(mapOf(ERROR_KEY to "Failed to decode JSON post message.")) 130 - return 131 - } 132 - this.onProgress(mapOf(PROGRESS_KEY to messageFloat)) 133 - } 134 - } 135 - } 136 - 137 - companion object { 138 - const val PROGRESS_KEY = "progress" 139 - const val ERROR_KEY = "message" 140 - } 141 - }
+1 -3
modules/expo-bluesky-swiss-army/expo-module.config.json
··· 5 5 "ExpoBlueskySharedPrefsModule", 6 6 "ExpoBlueskyReferrerModule", 7 7 "ExpoBlueskyVisibilityViewModule", 8 - "ExpoHLSDownloadModule", 9 8 "ExpoPlatformInfoModule" 10 9 ] 11 10 }, ··· 14 13 "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule", 15 14 "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule", 16 15 "expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule", 17 - "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule", 18 - "expo.modules.blueskyswissarmy.hlsdownload.ExpoHLSDownloadModule" 16 + "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule" 19 17 ] 20 18 } 21 19 }
+1 -9
modules/expo-bluesky-swiss-army/index.ts
··· 1 - import HLSDownloadView from './src/HLSDownload' 2 1 import * as PlatformInfo from './src/PlatformInfo' 3 2 import {AudioCategory} from './src/PlatformInfo/types' 4 3 import * as Referrer from './src/Referrer' 5 4 import * as SharedPrefs from './src/SharedPrefs' 6 5 import VisibilityView from './src/VisibilityView' 7 6 8 - export { 9 - AudioCategory, 10 - HLSDownloadView, 11 - PlatformInfo, 12 - Referrer, 13 - SharedPrefs, 14 - VisibilityView, 15 - } 7 + export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
-31
modules/expo-bluesky-swiss-army/ios/HLSDownload/ExpoHLSDownloadModule.swift
··· 1 - import ExpoModulesCore 2 - 3 - public class ExpoHLSDownloadModule: Module { 4 - public func definition() -> ModuleDefinition { 5 - Name("ExpoHLSDownload") 6 - 7 - Function("isAvailable") { 8 - if #available(iOS 14.5, *) { 9 - return true 10 - } 11 - return false 12 - } 13 - 14 - View(HLSDownloadView.self) { 15 - Events([ 16 - "onStart", 17 - "onError", 18 - "onProgress", 19 - "onSuccess" 20 - ]) 21 - 22 - Prop("downloaderUrl") { (view: HLSDownloadView, downloaderUrl: URL) in 23 - view.downloaderUrl = downloaderUrl 24 - } 25 - 26 - AsyncFunction("startDownloadAsync") { (view: HLSDownloadView, sourceUrl: URL) in 27 - view.startDownload(sourceUrl: sourceUrl) 28 - } 29 - } 30 - } 31 - }
-148
modules/expo-bluesky-swiss-army/ios/HLSDownload/HLSDownloadView.swift
··· 1 - import ExpoModulesCore 2 - import WebKit 3 - 4 - class HLSDownloadView: ExpoView, WKScriptMessageHandler, WKNavigationDelegate, WKDownloadDelegate { 5 - var webView: WKWebView! 6 - var downloaderUrl: URL? 7 - 8 - private var onStart = EventDispatcher() 9 - private var onError = EventDispatcher() 10 - private var onProgress = EventDispatcher() 11 - private var onSuccess = EventDispatcher() 12 - 13 - private var outputUrl: URL? 14 - 15 - public required init(appContext: AppContext? = nil) { 16 - super.init(appContext: appContext) 17 - 18 - // controller for post message api 19 - let contentController = WKUserContentController() 20 - contentController.add(self, name: "onMessage") 21 - let configuration = WKWebViewConfiguration() 22 - configuration.userContentController = contentController 23 - 24 - // create webview 25 - let webView = WKWebView(frame: .zero, configuration: configuration) 26 - 27 - // Use these for debugging, to see the webview itself 28 - webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 29 - webView.layer.masksToBounds = false 30 - webView.backgroundColor = .clear 31 - webView.contentMode = .scaleToFill 32 - 33 - webView.navigationDelegate = self 34 - 35 - self.addSubview(webView) 36 - self.webView = webView 37 - } 38 - 39 - required init?(coder: NSCoder) { 40 - fatalError("init(coder:) has not been implemented") 41 - } 42 - 43 - // MARK: - view functions 44 - 45 - func startDownload(sourceUrl: URL) { 46 - guard let downloaderUrl = self.downloaderUrl, 47 - let url = URL(string: "\(downloaderUrl.absoluteString)?videoUrl=\(sourceUrl.absoluteString)") else { 48 - self.onError([ 49 - "message": "Downloader URL is not set." 50 - ]) 51 - return 52 - } 53 - 54 - self.onStart() 55 - self.webView.load(URLRequest(url: url)) 56 - } 57 - 58 - // webview message handling 59 - 60 - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 61 - guard let response = message.body as? String, 62 - let data = response.data(using: .utf8), 63 - let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else { 64 - self.onError([ 65 - "message": "Failed to decode JSON post message." 66 - ]) 67 - return 68 - } 69 - 70 - switch payload.action { 71 - case .progress: 72 - guard let progress = payload.messageFloat else { 73 - self.onError([ 74 - "message": "Failed to decode JSON post message." 75 - ]) 76 - return 77 - } 78 - self.onProgress([ 79 - "progress": progress 80 - ]) 81 - case .error: 82 - guard let messageStr = payload.messageStr else { 83 - self.onError([ 84 - "message": "Failed to decode JSON post message." 85 - ]) 86 - return 87 - } 88 - self.onError([ 89 - "message": messageStr 90 - ]) 91 - } 92 - } 93 - 94 - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { 95 - guard #available(iOS 14.5, *) else { 96 - return .cancel 97 - } 98 - 99 - if navigationAction.shouldPerformDownload { 100 - return .download 101 - } else { 102 - return .allow 103 - } 104 - } 105 - 106 - // MARK: - wkdownloaddelegate 107 - 108 - @available(iOS 14.5, *) 109 - func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { 110 - download.delegate = self 111 - } 112 - 113 - @available(iOS 14.5, *) 114 - func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { 115 - download.delegate = self 116 - } 117 - 118 - @available(iOS 14.5, *) 119 - func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { 120 - let directory = NSTemporaryDirectory() 121 - let fileName = "\(NSUUID().uuidString).mp4" 122 - let url = NSURL.fileURL(withPathComponents: [directory, fileName]) 123 - 124 - self.outputUrl = url 125 - completionHandler(url) 126 - } 127 - 128 - @available(iOS 14.5, *) 129 - func downloadDidFinish(_ download: WKDownload) { 130 - guard let url = self.outputUrl else { 131 - return 132 - } 133 - self.onSuccess([ 134 - "uri": url.absoluteString 135 - ]) 136 - self.outputUrl = nil 137 - } 138 - } 139 - 140 - struct WebViewActionPayload: Decodable { 141 - enum Action: String, Decodable { 142 - case progress, error 143 - } 144 - 145 - let action: Action 146 - let messageStr: String? 147 - let messageFloat: Float? 148 - }
-39
modules/expo-bluesky-swiss-army/src/HLSDownload/index.native.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, ViewStyle} from 'react-native' 3 - import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' 4 - 5 - import {HLSDownloadViewProps} from './types' 6 - 7 - const NativeModule = requireNativeModule('ExpoHLSDownload') 8 - const NativeView: React.ComponentType< 9 - HLSDownloadViewProps & { 10 - ref: React.RefObject<any> 11 - style: StyleProp<ViewStyle> 12 - } 13 - > = requireNativeViewManager('ExpoHLSDownload') 14 - 15 - export default class HLSDownloadView extends React.PureComponent<HLSDownloadViewProps> { 16 - private nativeRef: React.RefObject<any> = React.createRef() 17 - 18 - constructor(props: HLSDownloadViewProps) { 19 - super(props) 20 - } 21 - 22 - static isAvailable(): boolean { 23 - return NativeModule.isAvailable() 24 - } 25 - 26 - async startDownloadAsync(sourceUrl: string): Promise<void> { 27 - return await this.nativeRef.current.startDownloadAsync(sourceUrl) 28 - } 29 - 30 - render() { 31 - return ( 32 - <NativeView 33 - ref={this.nativeRef} 34 - style={{height: 0, width: 0}} 35 - {...this.props} 36 - /> 37 - ) 38 - } 39 - }
-22
modules/expo-bluesky-swiss-army/src/HLSDownload/index.tsx
··· 1 - import React from 'react' 2 - 3 - import {NotImplementedError} from '../NotImplemented' 4 - import {HLSDownloadViewProps} from './types' 5 - 6 - export default class HLSDownloadView extends React.PureComponent<HLSDownloadViewProps> { 7 - constructor(props: HLSDownloadViewProps) { 8 - super(props) 9 - } 10 - 11 - static isAvailable(): boolean { 12 - return false 13 - } 14 - 15 - async startDownloadAsync(sourceUrl: string): Promise<void> { 16 - throw new NotImplementedError({sourceUrl}) 17 - } 18 - 19 - render() { 20 - return null 21 - } 22 - }
-10
modules/expo-bluesky-swiss-army/src/HLSDownload/types.ts
··· 1 - import {NativeSyntheticEvent} from 'react-native' 2 - 3 - export interface HLSDownloadViewProps { 4 - downloaderUrl: string 5 - onSuccess: (e: NativeSyntheticEvent<{uri: string}>) => void 6 - 7 - onStart?: () => void 8 - onError?: (e: NativeSyntheticEvent<{message: string}>) => void 9 - onProgress?: (e: NativeSyntheticEvent<{progress: number}>) => void 10 - }
-4
package.json
··· 59 59 "@emoji-mart/react": "^1.1.1", 60 60 "@expo/html-elements": "^0.4.2", 61 61 "@expo/webpack-config": "^19.0.0", 62 - "@ffmpeg/ffmpeg": "^0.12.10", 63 - "@ffmpeg/util": "^0.12.1", 64 62 "@floating-ui/dom": "^1.6.3", 65 63 "@floating-ui/react-dom": "^2.0.8", 66 64 "@formatjs/intl-locale": "^4.0.0", ··· 145 143 "expo-web-browser": "~13.0.3", 146 144 "fast-text-encoding": "^1.0.6", 147 145 "history": "^5.3.0", 148 - "hls-parser": "^0.13.3", 149 146 "hls.js": "^1.5.11", 150 147 "js-sha256": "^0.9.0", 151 148 "jwt-decode": "^4.0.0", ··· 227 224 "@testing-library/react-native": "^11.5.2", 228 225 "@tsconfig/react-native": "^2.0.3", 229 226 "@types/he": "^1.1.2", 230 - "@types/hls-parser": "^0.8.7", 231 227 "@types/jest": "^29.4.0", 232 228 "@types/lodash.chunk": "^4.2.7", 233 229 "@types/lodash.debounce": "^4.0.7",
-6
src/Navigation.tsx
··· 50 50 StarterPackScreenShort, 51 51 } from '#/screens/StarterPack/StarterPackScreen' 52 52 import {Wizard} from '#/screens/StarterPack/Wizard' 53 - import {VideoDownloadScreen} from '#/components/VideoDownloadScreen' 54 53 import {Referrer} from '../modules/expo-bluesky-swiss-army' 55 54 import {init as initAnalytics} from './lib/analytics/analytics' 56 55 import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' ··· 364 363 name="StarterPackEdit" 365 364 getComponent={() => Wizard} 366 365 options={{title: title(msg`Edit your starter pack`), requireAuth: true}} 367 - /> 368 - <Stack.Screen 369 - name="VideoDownload" 370 - getComponent={() => VideoDownloadScreen} 371 - options={{title: title(msg`Download video`)}} 372 366 /> 373 367 </> 374 368 )
-4
src/components/VideoDownloadScreen.native.tsx
··· 1 - export function VideoDownloadScreen() { 2 - // @TODO redirect 3 - return null 4 - }
-215
src/components/VideoDownloadScreen.tsx
··· 1 - import React from 'react' 2 - import {parse} from 'hls-parser' 3 - import {MasterPlaylist, MediaPlaylist, Variant} from 'hls-parser/types' 4 - 5 - interface PostMessageData { 6 - action: 'progress' | 'error' 7 - messageStr?: string 8 - messageFloat?: number 9 - } 10 - 11 - function postMessage(data: PostMessageData) { 12 - // @ts-expect-error safari webview only 13 - if (window?.webkit) { 14 - // @ts-expect-error safari webview only 15 - window.webkit.messageHandlers.onMessage.postMessage(JSON.stringify(data)) 16 - // @ts-expect-error android webview only 17 - } else if (AndroidInterface) { 18 - // @ts-expect-error android webview only 19 - AndroidInterface.onMessage(JSON.stringify(data)) 20 - } 21 - } 22 - 23 - function createSegementUrl(originalUrl: string, newFile: string) { 24 - const parts = originalUrl.split('/') 25 - parts[parts.length - 1] = newFile 26 - return parts.join('/') 27 - } 28 - 29 - export function VideoDownloadScreen() { 30 - const ffmpegRef = React.useRef<any>(null) 31 - const fetchFileRef = React.useRef<any>(null) 32 - 33 - const [dataUrl, setDataUrl] = React.useState<any>(null) 34 - 35 - const load = React.useCallback(async () => { 36 - const ffmpegLib = await import('@ffmpeg/ffmpeg') 37 - const ffmpeg = new ffmpegLib.FFmpeg() 38 - ffmpegRef.current = ffmpeg 39 - 40 - const ffmpegUtilLib = await import('@ffmpeg/util') 41 - fetchFileRef.current = ffmpegUtilLib.fetchFile 42 - 43 - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm' 44 - 45 - await ffmpeg.load({ 46 - coreURL: await ffmpegUtilLib.toBlobURL( 47 - `${baseURL}/ffmpeg-core.js`, 48 - 'text/javascript', 49 - ), 50 - wasmURL: await ffmpegUtilLib.toBlobURL( 51 - `${baseURL}/ffmpeg-core.wasm`, 52 - 'application/wasm', 53 - ), 54 - }) 55 - }, []) 56 - 57 - const createMp4 = React.useCallback(async (videoUrl: string) => { 58 - // Get the master playlist and find the best variant 59 - const masterPlaylistRes = await fetch(videoUrl) 60 - const masterPlaylistText = await masterPlaylistRes.text() 61 - const masterPlaylist = parse(masterPlaylistText) as MasterPlaylist 62 - 63 - // If URL given is not a master playlist, we probably cannot handle this. 64 - if (!masterPlaylist.isMasterPlaylist) { 65 - postMessage({ 66 - action: 'error', 67 - messageStr: 'A master playlist was not found in the provided playlist.', 68 - }) 69 - return 70 - } 71 - 72 - // Figure out what the best quality is. These should generally be in order, but we'll check them all just in case 73 - let bestVariant: Variant | undefined 74 - for (const variant of masterPlaylist.variants) { 75 - if (!bestVariant || variant.bandwidth > bestVariant.bandwidth) { 76 - bestVariant = variant 77 - } 78 - } 79 - 80 - // Should only happen if there was no variants at all given to us. Mostly for types. 81 - if (!bestVariant) { 82 - postMessage({ 83 - action: 'error', 84 - messageStr: 'No variants were found in the provided master playlist.', 85 - }) 86 - return 87 - } 88 - 89 - const urlParts = videoUrl.split('/') 90 - urlParts[urlParts.length - 1] = bestVariant?.uri 91 - const bestVariantUrl = urlParts.join('/') 92 - 93 - // Download and parse m3u8 94 - const hlsFileRes = await fetch(bestVariantUrl) 95 - const hlsPlainText = await hlsFileRes.text() 96 - const playlist = parse(hlsPlainText) as MediaPlaylist 97 - 98 - // This one shouldn't be a master playlist - again just for types really 99 - if (playlist.isMasterPlaylist) { 100 - postMessage({ 101 - action: 'error', 102 - messageStr: 'An unknown error has occurred.', 103 - }) 104 - return 105 - } 106 - 107 - const ffmpeg = ffmpegRef.current 108 - 109 - // Get the correctly ordered file names. We need to remove the tracking info from the end of the file name 110 - const segments = playlist.segments.map(segment => { 111 - return segment.uri.split('?')[0] 112 - }) 113 - 114 - // Download each segment 115 - let error: string | null = null 116 - let completed = 0 117 - await Promise.all( 118 - playlist.segments.map(async segment => { 119 - const uri = createSegementUrl(bestVariantUrl, segment.uri) 120 - const filename = segment.uri.split('?')[0] 121 - 122 - const res = await fetch(uri) 123 - if (!res.ok) { 124 - error = 'Failed to download playlist segment.' 125 - } 126 - 127 - const blob = await res.blob() 128 - try { 129 - await ffmpeg.writeFile(filename, await fetchFileRef.current(blob)) 130 - } catch (e: unknown) { 131 - error = 'Failed to write file.' 132 - } finally { 133 - completed++ 134 - const progress = completed / playlist.segments.length 135 - postMessage({ 136 - action: 'progress', 137 - messageFloat: progress, 138 - }) 139 - } 140 - }), 141 - ) 142 - 143 - // Do something if there was an error 144 - if (error) { 145 - postMessage({ 146 - action: 'error', 147 - messageStr: error, 148 - }) 149 - return 150 - } 151 - 152 - // Put the segments together 153 - await ffmpeg.exec([ 154 - '-i', 155 - `concat:${segments.join('|')}`, 156 - '-c:v', 157 - 'copy', 158 - 'output.mp4', 159 - ]) 160 - 161 - const fileData = await ffmpeg.readFile('output.mp4') 162 - const blob = new Blob([fileData.buffer], {type: 'video/mp4'}) 163 - const dataUrl = await new Promise<string | null>(resolve => { 164 - const reader = new FileReader() 165 - reader.onloadend = () => resolve(reader.result as string) 166 - reader.onerror = () => resolve(null) 167 - reader.readAsDataURL(blob) 168 - }) 169 - return dataUrl 170 - }, []) 171 - 172 - const download = React.useCallback( 173 - async (videoUrl: string) => { 174 - await load() 175 - const mp4Res = await createMp4(videoUrl) 176 - 177 - if (!mp4Res) { 178 - postMessage({ 179 - action: 'error', 180 - messageStr: 'An error occurred while creating the MP4.', 181 - }) 182 - return 183 - } 184 - 185 - setDataUrl(mp4Res) 186 - }, 187 - [createMp4, load], 188 - ) 189 - 190 - React.useEffect(() => { 191 - const url = new URL(window.location.href) 192 - const videoUrl = url.searchParams.get('videoUrl') 193 - 194 - if (!videoUrl) { 195 - postMessage({action: 'error', messageStr: 'No video URL provided'}) 196 - } else { 197 - setDataUrl(null) 198 - download(videoUrl) 199 - } 200 - }, [download]) 201 - 202 - if (!dataUrl) return null 203 - 204 - return ( 205 - <div> 206 - <a 207 - href={dataUrl} 208 - ref={el => { 209 - el?.click() 210 - }} 211 - download="video.mp4" 212 - /> 213 - </div> 214 - ) 215 - }
-1
src/lib/routes/types.ts
··· 50 50 StarterPackShort: {code: string} 51 51 StarterPackWizard: undefined 52 52 StarterPackEdit: {rkey?: string} 53 - VideoDownload: undefined 54 53 } 55 54 56 55 export type BottomTabNavigatorParams = CommonNavigatorParams & {
-1
src/routes.ts
··· 48 48 StarterPack: '/starter-pack/:name/:rkey', 49 49 StarterPackShort: '/starter-pack-short/:code', 50 50 StarterPackWizard: '/starter-pack/create', 51 - VideoDownload: '/video-download', 52 51 })
+1 -45
src/view/screens/Storybook/index.tsx
··· 1 1 import React from 'react' 2 2 import {ScrollView, View} from 'react-native' 3 - import {deleteAsync} from 'expo-file-system' 4 - import {saveToLibraryAsync} from 'expo-media-library' 5 3 6 4 import {useSetThemePrefs} from '#/state/shell' 7 - import {useVideoLibraryPermission} from 'lib/hooks/usePermissions' 8 - import {isIOS, isWeb} from 'platform/detection' 5 + import {isWeb} from 'platform/detection' 9 6 import {CenteredView} from '#/view/com/util/Views' 10 - import * as Toast from 'view/com/util/Toast' 11 7 import {ListContained} from 'view/screens/Storybook/ListContained' 12 8 import {atoms as a, ThemeProvider, useTheme} from '#/alf' 13 9 import {Button, ButtonText} from '#/components/Button' 14 - import {HLSDownloadView} from '../../../../modules/expo-bluesky-swiss-army' 15 10 import {Breakpoints} from './Breakpoints' 16 11 import {Buttons} from './Buttons' 17 12 import {Dialogs} from './Dialogs' ··· 38 33 const t = useTheme() 39 34 const {setColorMode, setDarkTheme} = useSetThemePrefs() 40 35 const [showContainedList, setShowContainedList] = React.useState(false) 41 - const hlsDownloadRef = React.useRef<HLSDownloadView>(null) 42 - 43 - const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() 44 36 45 37 return ( 46 38 <CenteredView style={[t.atoms.bg]}> 47 39 <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 100}]}> 48 - <HLSDownloadView 49 - ref={hlsDownloadRef} 50 - downloaderUrl={ 51 - isIOS 52 - ? 'http://localhost:19006/video-download' 53 - : 'http://10.0.2.2:19006/video-download' 54 - } 55 - onSuccess={async e => { 56 - const uri = e.nativeEvent.uri 57 - const permsRes = await requestVideoAccessIfNeeded() 58 - if (!permsRes) return 59 - 60 - await saveToLibraryAsync(uri) 61 - try { 62 - deleteAsync(uri) 63 - } catch (err) { 64 - console.error('Failed to delete file', err) 65 - } 66 - Toast.show('Video saved to library') 67 - }} 68 - onStart={() => console.log('Download is starting')} 69 - onError={e => console.log(e.nativeEvent.message)} 70 - onProgress={e => console.log(e.nativeEvent.progress)} 71 - /> 72 - <Button 73 - variant="solid" 74 - color="primary" 75 - size="small" 76 - onPress={async () => { 77 - hlsDownloadRef.current?.startDownloadAsync( 78 - 'https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?download=true', 79 - ) 80 - }} 81 - label="Video download test"> 82 - <ButtonText>Video download test</ButtonText> 83 - </Button> 84 40 {!showContainedList ? ( 85 41 <> 86 42 <View style={[a.flex_row, a.align_start, a.gap_md]}>
-29
yarn.lock
··· 3925 3925 resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" 3926 3926 integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== 3927 3927 3928 - "@ffmpeg/ffmpeg@^0.12.10": 3929 - version "0.12.10" 3930 - resolved "https://registry.yarnpkg.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz#e3cce21f21f11f33dfc1ec1d5ad5694f4a3073c9" 3931 - integrity sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ== 3932 - dependencies: 3933 - "@ffmpeg/types" "^0.12.2" 3934 - 3935 - "@ffmpeg/types@^0.12.2": 3936 - version "0.12.2" 3937 - resolved "https://registry.yarnpkg.com/@ffmpeg/types/-/types-0.12.2.tgz#bc7eef321ae50225c247091f1f23fd3087c6aa1d" 3938 - integrity sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA== 3939 - 3940 - "@ffmpeg/util@^0.12.1": 3941 - version "0.12.1" 3942 - resolved "https://registry.yarnpkg.com/@ffmpeg/util/-/util-0.12.1.tgz#98afa20d7b4c0821eebdb205ddcfa5d07b0a4f53" 3943 - integrity sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ== 3944 - 3945 3928 "@floating-ui/core@^1.0.0": 3946 3929 version "1.6.0" 3947 3930 resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" ··· 8023 8006 version "1.2.0" 8024 8007 resolved "https://registry.yarnpkg.com/@types/he/-/he-1.2.0.tgz#3845193e597d943bab4e61ca5d7f3d8fc3d572a3" 8025 8008 integrity sha512-uH2smqTN4uGReAiKedIVzoLUAXIYLBTbSofhx3hbNqj74Ua6KqFsLYszduTrLCMEAEAozF73DbGi/SC1bzQq4g== 8026 - 8027 - "@types/hls-parser@^0.8.7": 8028 - version "0.8.7" 8029 - resolved "https://registry.yarnpkg.com/@types/hls-parser/-/hls-parser-0.8.7.tgz#26360493231ed8606ebe995976c63c69c3982657" 8030 - integrity sha512-3ry9V6i/uhSbNdvBUENAqt2p5g+xKIbjkr5Qv4EaXe7eIJnaGQntFZalRLQlKoEop381a0LwUr2qNKKlxQC4TQ== 8031 - dependencies: 8032 - "@types/node" "*" 8033 8009 8034 8010 "@types/html-minifier-terser@^6.0.0": 8035 8011 version "6.1.0" ··· 13479 13455 integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== 13480 13456 dependencies: 13481 13457 "@babel/runtime" "^7.7.6" 13482 - 13483 - hls-parser@^0.13.3: 13484 - version "0.13.3" 13485 - resolved "https://registry.yarnpkg.com/hls-parser/-/hls-parser-0.13.3.tgz#5f7a305629cf462bbf16a4d080e03e0be714f1fe" 13486 - integrity sha512-DXqW7bwx9j2qFcAXS/LBJTDJWitxknb6oUnsnTvECHrecPvPbhRgIu45OgNDUU6gpwKxMJx40SHRRUUhdIM2gA== 13487 13458 13488 13459 hls.js@^1.5.11: 13489 13460 version "1.5.11"