Revert "[Video] Download videos" (#4945)
This commit is contained in:
		
							parent
							
								
									b6e515c664
								
							
						
					
					
						commit
						a5af24b53b
					
				
					 19 changed files with 3 additions and 747 deletions
				
			
		| 
						 | 
				
			
			@ -256,9 +256,6 @@ func serve(cctx *cli.Context) error {
 | 
			
		|||
	e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
 | 
			
		||||
	e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
 | 
			
		||||
 | 
			
		||||
	// video download
 | 
			
		||||
	e.GET("/video-download", server.WebGeneric)
 | 
			
		||||
 | 
			
		||||
	// starter packs
 | 
			
		||||
	e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
 | 
			
		||||
	e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,4 +7,3 @@
 | 
			
		|||
# be ok.
 | 
			
		||||
User-Agent: *
 | 
			
		||||
Allow: /
 | 
			
		||||
Disallow: /video-download
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
package expo.modules.blueskyswissarmy.hlsdownload
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import expo.modules.kotlin.modules.Module
 | 
			
		||||
import expo.modules.kotlin.modules.ModuleDefinition
 | 
			
		||||
 | 
			
		||||
class ExpoHLSDownloadModule : Module() {
 | 
			
		||||
  override fun definition() =
 | 
			
		||||
    ModuleDefinition {
 | 
			
		||||
      Name("ExpoHLSDownload")
 | 
			
		||||
 | 
			
		||||
      Function("isAvailable") {
 | 
			
		||||
        return@Function true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      View(HLSDownloadView::class) {
 | 
			
		||||
        Events(
 | 
			
		||||
          arrayOf(
 | 
			
		||||
            "onStart",
 | 
			
		||||
            "onError",
 | 
			
		||||
            "onProgress",
 | 
			
		||||
            "onSuccess",
 | 
			
		||||
          ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        Prop("downloaderUrl") { view: HLSDownloadView, downloaderUrl: Uri ->
 | 
			
		||||
          view.downloaderUrl = downloaderUrl
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        AsyncFunction("startDownloadAsync") { view: HLSDownloadView, sourceUrl: Uri ->
 | 
			
		||||
          view.startDownload(sourceUrl)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,141 +0,0 @@
 | 
			
		|||
package expo.modules.blueskyswissarmy.hlsdownload
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.util.Base64
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import android.webkit.DownloadListener
 | 
			
		||||
import android.webkit.JavascriptInterface
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import expo.modules.kotlin.AppContext
 | 
			
		||||
import expo.modules.kotlin.viewevent.EventDispatcher
 | 
			
		||||
import expo.modules.kotlin.viewevent.ViewEventCallback
 | 
			
		||||
import expo.modules.kotlin.views.ExpoView
 | 
			
		||||
import org.json.JSONObject
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileOutputStream
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.util.UUID
 | 
			
		||||
 | 
			
		||||
class HLSDownloadView(
 | 
			
		||||
  context: Context,
 | 
			
		||||
  appContext: AppContext,
 | 
			
		||||
) : ExpoView(context, appContext),
 | 
			
		||||
  DownloadListener {
 | 
			
		||||
  private val webView = WebView(context)
 | 
			
		||||
 | 
			
		||||
  var downloaderUrl: Uri? = null
 | 
			
		||||
 | 
			
		||||
  private val onStart by EventDispatcher()
 | 
			
		||||
  private val onError by EventDispatcher()
 | 
			
		||||
  private val onProgress by EventDispatcher()
 | 
			
		||||
  private val onSuccess by EventDispatcher()
 | 
			
		||||
 | 
			
		||||
  init {
 | 
			
		||||
    this.setupWebView()
 | 
			
		||||
    this.addView(this.webView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @SuppressLint("SetJavaScriptEnabled")
 | 
			
		||||
  private fun setupWebView() {
 | 
			
		||||
    val webSettings = this.webView.settings
 | 
			
		||||
    webSettings.javaScriptEnabled = true
 | 
			
		||||
    webSettings.domStorageEnabled = true
 | 
			
		||||
 | 
			
		||||
    webView.setDownloadListener(this)
 | 
			
		||||
    webView.addJavascriptInterface(WebAppInterface(this.onProgress, this.onError), "AndroidInterface")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun onDetachedFromWindow() {
 | 
			
		||||
    super.onDetachedFromWindow()
 | 
			
		||||
    this.webView.stopLoading()
 | 
			
		||||
    this.webView.clearHistory()
 | 
			
		||||
    this.webView.removeAllViews()
 | 
			
		||||
    this.webView.destroy()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fun startDownload(sourceUrl: Uri) {
 | 
			
		||||
    if (this.downloaderUrl == null) {
 | 
			
		||||
      this.onError(mapOf(ERROR_KEY to "Downloader URL is not set."))
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val url = URI("${this.downloaderUrl}?videoUrl=$sourceUrl")
 | 
			
		||||
    this.webView.loadUrl(url.toString())
 | 
			
		||||
    this.onStart(mapOf())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun onDownloadStart(
 | 
			
		||||
    url: String?,
 | 
			
		||||
    userAgent: String?,
 | 
			
		||||
    contentDisposition: String?,
 | 
			
		||||
    mimeType: String?,
 | 
			
		||||
    contentLength: Long,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (url == null) {
 | 
			
		||||
      this.onError(mapOf(ERROR_KEY to "Failed to retrieve download URL from webview."))
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val tempDir = context.cacheDir
 | 
			
		||||
    val fileName = "${UUID.randomUUID()}.mp4"
 | 
			
		||||
    val file = File(tempDir, fileName)
 | 
			
		||||
 | 
			
		||||
    val base64 = url.split(",")[1]
 | 
			
		||||
    val bytes = Base64.decode(base64, Base64.DEFAULT)
 | 
			
		||||
 | 
			
		||||
    val fos = FileOutputStream(file)
 | 
			
		||||
    try {
 | 
			
		||||
      fos.write(bytes)
 | 
			
		||||
    } catch (e: Exception) {
 | 
			
		||||
      Log.e("FileDownload", "Error downloading file", e)
 | 
			
		||||
      this.onError(mapOf(ERROR_KEY to e.message.toString()))
 | 
			
		||||
      return
 | 
			
		||||
    } finally {
 | 
			
		||||
      fos.close()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val uri = Uri.fromFile(file)
 | 
			
		||||
    this.onSuccess(mapOf("uri" to uri.toString()))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    const val ERROR_KEY = "message"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class WebAppInterface(
 | 
			
		||||
  val onProgress: ViewEventCallback<Map<String, Any>>,
 | 
			
		||||
  val onError: ViewEventCallback<Map<String, Any>>,
 | 
			
		||||
) {
 | 
			
		||||
  @JavascriptInterface
 | 
			
		||||
  public fun onMessage(message: String) {
 | 
			
		||||
    val jsonObject = JSONObject(message)
 | 
			
		||||
    val action = jsonObject.getString("action")
 | 
			
		||||
 | 
			
		||||
    when (action) {
 | 
			
		||||
      "error" -> {
 | 
			
		||||
        val messageStr = jsonObject.get("messageStr")
 | 
			
		||||
        if (messageStr !is String) {
 | 
			
		||||
          this.onError(mapOf(ERROR_KEY to "Failed to decode JSON post message."))
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        this.onError(mapOf(ERROR_KEY to messageStr))
 | 
			
		||||
      }
 | 
			
		||||
      "progress" -> {
 | 
			
		||||
        val messageFloat = jsonObject.get("messageFloat")
 | 
			
		||||
        if (messageFloat !is Number) {
 | 
			
		||||
          this.onError(mapOf(ERROR_KEY to "Failed to decode JSON post message."))
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        this.onProgress(mapOf(PROGRESS_KEY to messageFloat))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    const val PROGRESS_KEY = "progress"
 | 
			
		||||
    const val ERROR_KEY = "message"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,6 @@
 | 
			
		|||
      "ExpoBlueskySharedPrefsModule",
 | 
			
		||||
      "ExpoBlueskyReferrerModule",
 | 
			
		||||
      "ExpoBlueskyVisibilityViewModule",
 | 
			
		||||
      "ExpoHLSDownloadModule",
 | 
			
		||||
      "ExpoPlatformInfoModule"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +13,7 @@
 | 
			
		|||
      "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
 | 
			
		||||
      "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule",
 | 
			
		||||
      "expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule",
 | 
			
		||||
      "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule",
 | 
			
		||||
      "expo.modules.blueskyswissarmy.hlsdownload.ExpoHLSDownloadModule"
 | 
			
		||||
      "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,7 @@
 | 
			
		|||
import HLSDownloadView from './src/HLSDownload'
 | 
			
		||||
import * as PlatformInfo from './src/PlatformInfo'
 | 
			
		||||
import {AudioCategory} from './src/PlatformInfo/types'
 | 
			
		||||
import * as Referrer from './src/Referrer'
 | 
			
		||||
import * as SharedPrefs from './src/SharedPrefs'
 | 
			
		||||
import VisibilityView from './src/VisibilityView'
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  AudioCategory,
 | 
			
		||||
  HLSDownloadView,
 | 
			
		||||
  PlatformInfo,
 | 
			
		||||
  Referrer,
 | 
			
		||||
  SharedPrefs,
 | 
			
		||||
  VisibilityView,
 | 
			
		||||
}
 | 
			
		||||
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
import ExpoModulesCore
 | 
			
		||||
 | 
			
		||||
public class ExpoHLSDownloadModule: Module {
 | 
			
		||||
  public func definition() -> ModuleDefinition {
 | 
			
		||||
    Name("ExpoHLSDownload")
 | 
			
		||||
 | 
			
		||||
    Function("isAvailable") {
 | 
			
		||||
      if #available(iOS 14.5, *) {
 | 
			
		||||
        return true
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    View(HLSDownloadView.self) {
 | 
			
		||||
      Events([
 | 
			
		||||
        "onStart",
 | 
			
		||||
        "onError",
 | 
			
		||||
        "onProgress",
 | 
			
		||||
        "onSuccess"
 | 
			
		||||
      ])
 | 
			
		||||
 | 
			
		||||
      Prop("downloaderUrl") { (view: HLSDownloadView, downloaderUrl: URL) in
 | 
			
		||||
        view.downloaderUrl = downloaderUrl
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      AsyncFunction("startDownloadAsync") { (view: HLSDownloadView, sourceUrl: URL) in
 | 
			
		||||
        view.startDownload(sourceUrl: sourceUrl)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,148 +0,0 @@
 | 
			
		|||
import ExpoModulesCore
 | 
			
		||||
import WebKit
 | 
			
		||||
 | 
			
		||||
class HLSDownloadView: ExpoView, WKScriptMessageHandler, WKNavigationDelegate, WKDownloadDelegate {
 | 
			
		||||
  var webView: WKWebView!
 | 
			
		||||
  var downloaderUrl: URL?
 | 
			
		||||
 | 
			
		||||
  private var onStart = EventDispatcher()
 | 
			
		||||
  private var onError = EventDispatcher()
 | 
			
		||||
  private var onProgress = EventDispatcher()
 | 
			
		||||
  private var onSuccess = EventDispatcher()
 | 
			
		||||
 | 
			
		||||
  private var outputUrl: URL?
 | 
			
		||||
 | 
			
		||||
  public required init(appContext: AppContext? = nil) {
 | 
			
		||||
    super.init(appContext: appContext)
 | 
			
		||||
 | 
			
		||||
    // controller for post message api
 | 
			
		||||
    let contentController = WKUserContentController()
 | 
			
		||||
    contentController.add(self, name: "onMessage")
 | 
			
		||||
    let configuration = WKWebViewConfiguration()
 | 
			
		||||
    configuration.userContentController = contentController
 | 
			
		||||
 | 
			
		||||
    // create webview
 | 
			
		||||
    let webView = WKWebView(frame: .zero, configuration: configuration)
 | 
			
		||||
 | 
			
		||||
    // Use these for debugging, to see the webview itself
 | 
			
		||||
    webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
 | 
			
		||||
    webView.layer.masksToBounds = false
 | 
			
		||||
    webView.backgroundColor = .clear
 | 
			
		||||
    webView.contentMode = .scaleToFill
 | 
			
		||||
 | 
			
		||||
    webView.navigationDelegate = self
 | 
			
		||||
 | 
			
		||||
    self.addSubview(webView)
 | 
			
		||||
    self.webView = webView
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  required init?(coder: NSCoder) {
 | 
			
		||||
    fatalError("init(coder:) has not been implemented")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // MARK: - view functions
 | 
			
		||||
 | 
			
		||||
  func startDownload(sourceUrl: URL) {
 | 
			
		||||
    guard let downloaderUrl = self.downloaderUrl,
 | 
			
		||||
          let url = URL(string: "\(downloaderUrl.absoluteString)?videoUrl=\(sourceUrl.absoluteString)") else {
 | 
			
		||||
      self.onError([
 | 
			
		||||
        "message": "Downloader URL is not set."
 | 
			
		||||
      ])
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.onStart()
 | 
			
		||||
    self.webView.load(URLRequest(url: url))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // webview message handling
 | 
			
		||||
 | 
			
		||||
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
 | 
			
		||||
    guard let response = message.body as? String,
 | 
			
		||||
          let data = response.data(using: .utf8),
 | 
			
		||||
          let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
 | 
			
		||||
      self.onError([
 | 
			
		||||
        "message": "Failed to decode JSON post message."
 | 
			
		||||
      ])
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch payload.action {
 | 
			
		||||
    case .progress:
 | 
			
		||||
      guard let progress = payload.messageFloat else {
 | 
			
		||||
        self.onError([
 | 
			
		||||
          "message": "Failed to decode JSON post message."
 | 
			
		||||
        ])
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      self.onProgress([
 | 
			
		||||
        "progress": progress
 | 
			
		||||
      ])
 | 
			
		||||
      case .error:
 | 
			
		||||
      guard let messageStr = payload.messageStr else {
 | 
			
		||||
        self.onError([
 | 
			
		||||
          "message": "Failed to decode JSON post message."
 | 
			
		||||
        ])
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      self.onError([
 | 
			
		||||
        "message": messageStr
 | 
			
		||||
      ])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
 | 
			
		||||
    guard #available(iOS 14.5, *) else {
 | 
			
		||||
      return .cancel
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if navigationAction.shouldPerformDownload {
 | 
			
		||||
      return .download
 | 
			
		||||
    } else {
 | 
			
		||||
      return .allow
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // MARK: - wkdownloaddelegate
 | 
			
		||||
 | 
			
		||||
  @available(iOS 14.5, *)
 | 
			
		||||
  func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
 | 
			
		||||
    download.delegate = self
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @available(iOS 14.5, *)
 | 
			
		||||
  func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
 | 
			
		||||
    download.delegate = self
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @available(iOS 14.5, *)
 | 
			
		||||
  func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
 | 
			
		||||
    let directory = NSTemporaryDirectory()
 | 
			
		||||
    let fileName = "\(NSUUID().uuidString).mp4"
 | 
			
		||||
    let url = NSURL.fileURL(withPathComponents: [directory, fileName])
 | 
			
		||||
 | 
			
		||||
    self.outputUrl = url
 | 
			
		||||
    completionHandler(url)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @available(iOS 14.5, *)
 | 
			
		||||
  func downloadDidFinish(_ download: WKDownload) {
 | 
			
		||||
    guard let url = self.outputUrl else {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    self.onSuccess([
 | 
			
		||||
      "uri": url.absoluteString
 | 
			
		||||
    ])
 | 
			
		||||
    self.outputUrl = nil
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct WebViewActionPayload: Decodable {
 | 
			
		||||
  enum Action: String, Decodable {
 | 
			
		||||
    case progress, error
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let action: Action
 | 
			
		||||
  let messageStr: String?
 | 
			
		||||
  let messageFloat: Float?
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,39 +0,0 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import {StyleProp, ViewStyle} from 'react-native'
 | 
			
		||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
 | 
			
		||||
 | 
			
		||||
import {HLSDownloadViewProps} from './types'
 | 
			
		||||
 | 
			
		||||
const NativeModule = requireNativeModule('ExpoHLSDownload')
 | 
			
		||||
const NativeView: React.ComponentType<
 | 
			
		||||
  HLSDownloadViewProps & {
 | 
			
		||||
    ref: React.RefObject<any>
 | 
			
		||||
    style: StyleProp<ViewStyle>
 | 
			
		||||
  }
 | 
			
		||||
> = requireNativeViewManager('ExpoHLSDownload')
 | 
			
		||||
 | 
			
		||||
export default class HLSDownloadView extends React.PureComponent<HLSDownloadViewProps> {
 | 
			
		||||
  private nativeRef: React.RefObject<any> = React.createRef()
 | 
			
		||||
 | 
			
		||||
  constructor(props: HLSDownloadViewProps) {
 | 
			
		||||
    super(props)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isAvailable(): boolean {
 | 
			
		||||
    return NativeModule.isAvailable()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async startDownloadAsync(sourceUrl: string): Promise<void> {
 | 
			
		||||
    return await this.nativeRef.current.startDownloadAsync(sourceUrl)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <NativeView
 | 
			
		||||
        ref={this.nativeRef}
 | 
			
		||||
        style={{height: 0, width: 0}}
 | 
			
		||||
        {...this.props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +0,0 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import {NotImplementedError} from '../NotImplemented'
 | 
			
		||||
import {HLSDownloadViewProps} from './types'
 | 
			
		||||
 | 
			
		||||
export default class HLSDownloadView extends React.PureComponent<HLSDownloadViewProps> {
 | 
			
		||||
  constructor(props: HLSDownloadViewProps) {
 | 
			
		||||
    super(props)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isAvailable(): boolean {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async startDownloadAsync(sourceUrl: string): Promise<void> {
 | 
			
		||||
    throw new NotImplementedError({sourceUrl})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
import {NativeSyntheticEvent} from 'react-native'
 | 
			
		||||
 | 
			
		||||
export interface HLSDownloadViewProps {
 | 
			
		||||
  downloaderUrl: string
 | 
			
		||||
  onSuccess: (e: NativeSyntheticEvent<{uri: string}>) => void
 | 
			
		||||
 | 
			
		||||
  onStart?: () => void
 | 
			
		||||
  onError?: (e: NativeSyntheticEvent<{message: string}>) => void
 | 
			
		||||
  onProgress?: (e: NativeSyntheticEvent<{progress: number}>) => void
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -59,8 +59,6 @@
 | 
			
		|||
    "@emoji-mart/react": "^1.1.1",
 | 
			
		||||
    "@expo/html-elements": "^0.4.2",
 | 
			
		||||
    "@expo/webpack-config": "^19.0.0",
 | 
			
		||||
    "@ffmpeg/ffmpeg": "^0.12.10",
 | 
			
		||||
    "@ffmpeg/util": "^0.12.1",
 | 
			
		||||
    "@floating-ui/dom": "^1.6.3",
 | 
			
		||||
    "@floating-ui/react-dom": "^2.0.8",
 | 
			
		||||
    "@formatjs/intl-locale": "^4.0.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +143,6 @@
 | 
			
		|||
    "expo-web-browser": "~13.0.3",
 | 
			
		||||
    "fast-text-encoding": "^1.0.6",
 | 
			
		||||
    "history": "^5.3.0",
 | 
			
		||||
    "hls-parser": "^0.13.3",
 | 
			
		||||
    "hls.js": "^1.5.11",
 | 
			
		||||
    "js-sha256": "^0.9.0",
 | 
			
		||||
    "jwt-decode": "^4.0.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +224,6 @@
 | 
			
		|||
    "@testing-library/react-native": "^11.5.2",
 | 
			
		||||
    "@tsconfig/react-native": "^2.0.3",
 | 
			
		||||
    "@types/he": "^1.1.2",
 | 
			
		||||
    "@types/hls-parser": "^0.8.7",
 | 
			
		||||
    "@types/jest": "^29.4.0",
 | 
			
		||||
    "@types/lodash.chunk": "^4.2.7",
 | 
			
		||||
    "@types/lodash.debounce": "^4.0.7",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`)}}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export function VideoDownloadScreen() {
 | 
			
		||||
  // @TODO redirect
 | 
			
		||||
  return 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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,6 @@ export type CommonNavigatorParams = {
 | 
			
		|||
  StarterPackShort: {code: string}
 | 
			
		||||
  StarterPackWizard: undefined
 | 
			
		||||
  StarterPackEdit: {rkey?: string}
 | 
			
		||||
  VideoDownload: undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										29
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -3925,23 +3925,6 @@
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a"
 | 
			
		||||
  integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==
 | 
			
		||||
 | 
			
		||||
"@ffmpeg/ffmpeg@^0.12.10":
 | 
			
		||||
  version "0.12.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz#e3cce21f21f11f33dfc1ec1d5ad5694f4a3073c9"
 | 
			
		||||
  integrity sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@ffmpeg/types" "^0.12.2"
 | 
			
		||||
 | 
			
		||||
"@ffmpeg/types@^0.12.2":
 | 
			
		||||
  version "0.12.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ffmpeg/types/-/types-0.12.2.tgz#bc7eef321ae50225c247091f1f23fd3087c6aa1d"
 | 
			
		||||
  integrity sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==
 | 
			
		||||
 | 
			
		||||
"@ffmpeg/util@^0.12.1":
 | 
			
		||||
  version "0.12.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ffmpeg/util/-/util-0.12.1.tgz#98afa20d7b4c0821eebdb205ddcfa5d07b0a4f53"
 | 
			
		||||
  integrity sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==
 | 
			
		||||
 | 
			
		||||
"@floating-ui/core@^1.0.0":
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
 | 
			
		||||
| 
						 | 
				
			
			@ -8024,13 +8007,6 @@
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/@types/he/-/he-1.2.0.tgz#3845193e597d943bab4e61ca5d7f3d8fc3d572a3"
 | 
			
		||||
  integrity sha512-uH2smqTN4uGReAiKedIVzoLUAXIYLBTbSofhx3hbNqj74Ua6KqFsLYszduTrLCMEAEAozF73DbGi/SC1bzQq4g==
 | 
			
		||||
 | 
			
		||||
"@types/hls-parser@^0.8.7":
 | 
			
		||||
  version "0.8.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/hls-parser/-/hls-parser-0.8.7.tgz#26360493231ed8606ebe995976c63c69c3982657"
 | 
			
		||||
  integrity sha512-3ry9V6i/uhSbNdvBUENAqt2p5g+xKIbjkr5Qv4EaXe7eIJnaGQntFZalRLQlKoEop381a0LwUr2qNKKlxQC4TQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
 | 
			
		||||
"@types/html-minifier-terser@^6.0.0":
 | 
			
		||||
  version "6.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
 | 
			
		||||
| 
						 | 
				
			
			@ -13480,11 +13456,6 @@ history@^5.3.0:
 | 
			
		|||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.7.6"
 | 
			
		||||
 | 
			
		||||
hls-parser@^0.13.3:
 | 
			
		||||
  version "0.13.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hls-parser/-/hls-parser-0.13.3.tgz#5f7a305629cf462bbf16a4d080e03e0be714f1fe"
 | 
			
		||||
  integrity sha512-DXqW7bwx9j2qFcAXS/LBJTDJWitxknb6oUnsnTvECHrecPvPbhRgIu45OgNDUU6gpwKxMJx40SHRRUUhdIM2gA==
 | 
			
		||||
 | 
			
		||||
hls.js@^1.5.11:
 | 
			
		||||
  version "1.5.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.11.tgz#3941347df454983859ae8c75fe19e8818719a826"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue