-3
bskyweb/cmd/bskyweb/server.go
-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
-1
bskyweb/static/robots.txt
-35
modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/hlsdownload/ExpoHLSDownloadModule.kt
-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
-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
+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
-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
-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
-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
-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
-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
-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
-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",
-4
src/components/VideoDownloadScreen.native.tsx
-4
src/components/VideoDownloadScreen.native.tsx
-215
src/components/VideoDownloadScreen.tsx
-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
-1
src/lib/routes/types.ts
-1
src/routes.ts
-1
src/routes.ts
+1
-45
src/view/screens/Storybook/index.tsx
+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
-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"