如何从 Facebook 视频 URL 中提取可用的视频分辨率?

How to extract available video resolutions from Facebook video URL?

在我的 Facebook Video Downloader android 应用程序中,我想显示 SDHD 等视频分辨率和大小.目前我正在使用 InputStreamReaderPattern.compile 方法来查找 SDHD URL 的视频。 这种方法很少让我 HD link 的视频并且只提供 SD URL 可以下载。

下面是我link解析的代码

fun linkParsing(url: String, loaded: (item: DownloadItem) -> Unit) {
    val showLogs: Boolean = true
    Log.e("post_url", url)
    return try {
        val getUrl = URL(url)
        val urlConnection =
            getUrl.openConnection() as HttpURLConnection
        var reader: BufferedReader? = null
        urlConnection.setRequestProperty("User-Agent", POST_USER_AGENT)
        urlConnection.setRequestProperty("Accept", "*/*")
        val streamMap = StringBuilder()
        try {
            reader =
                BufferedReader(InputStreamReader(urlConnection.inputStream))
            var line: String?
            while (reader.readLine().also {
                    line = it
                } != null) {
                streamMap.append(line)
            }
        } catch (E: Exception) {
            E.printStackTrace()
            reader?.close()
            urlConnection.disconnect()
        } finally {
            reader?.close()
            urlConnection.disconnect()
        }
        if (streamMap.toString().contains("You must log in to continue.")) {
        } else {
            val metaTAGTitle =
                Pattern.compile("<meta property=\"og:title\"(.+?)\" />")
            val metaTAGTitleMatcher = metaTAGTitle.matcher(streamMap)
            val metaTAGDescription =
                Pattern.compile("<meta property=\"og:description\"(.+?)\" />")
            val metaTAGDescriptionMatcher =
                metaTAGDescription.matcher(streamMap)
            var authorName: String? = ""
            var fileName: String? = ""
            if (metaTAGTitleMatcher.find()) {
                var author =
                    streamMap.substring(metaTAGTitleMatcher.start(), metaTAGTitleMatcher.end())
                Log.e("Extractor", "AUTHOR :: $author")
                author = author.replace("<meta property=\"og:title\" content=\"", "")
                    .replace("\" />", "")
                authorName = author
            } else {
                authorName = "N/A"
            }
            if (metaTAGDescriptionMatcher.find()) {
                var name = streamMap.substring(
                    metaTAGDescriptionMatcher.start(),
                    metaTAGDescriptionMatcher.end()
                )
                Log.e("Extractor", "FILENAME :: $name")
                name = name.replace("<meta property=\"og:description\" content=\"", "")
                    .replace("\" />", "")
                fileName = name
            } else {
                fileName = "N/A"
            }
            val sdVideo =
                Pattern.compile("<meta property=\"og:video\"(.+?)\" />")
            val sdVideoMatcher = sdVideo.matcher(streamMap)
            val imagePattern =
                Pattern.compile("<meta property=\"og:image\"(.+?)\" />")
            val imageMatcher = imagePattern.matcher(streamMap)
            val thumbnailPattern =
                Pattern.compile("<img class=\"_3chq\" src=\"(.+?)\" />")
            val thumbnailMatcher = thumbnailPattern.matcher(streamMap)
            val hdVideo = Pattern.compile("(hd_src):\"(.+?)\"")
            val hdVideoMatcher = hdVideo.matcher(streamMap)
            val facebookFile = DownloadItem()
            facebookFile?.author = authorName
            facebookFile?.filename = fileName
            facebookFile?.postLink = url
            if (sdVideoMatcher.find()) {
                var vUrl = sdVideoMatcher.group()
                vUrl = vUrl.substring(8, vUrl.length - 1) //sd_scr: 8 char
                facebookFile?.sdUrl = vUrl
                facebookFile?.ext = "mp4"
                var imageUrl = streamMap.substring(sdVideoMatcher.start(), sdVideoMatcher.end())
                imageUrl = imageUrl.replace("<meta property=\"og:video\" content=\"", "")
                    .replace("\" />", "").replace("&amp;", "&")
                Log.e("Extractor", "FILENAME :: NULL")
                Log.e("Extractor", "FILENAME :: $imageUrl")
                facebookFile?.sdUrl = URLDecoder.decode(imageUrl, "UTF-8")
                if (showLogs) {
                    Log.e("Extractor", "SD_URL :: Null")
                    Log.e("Extractor", "SD_URL :: $imageUrl")
                }
                if (thumbnailMatcher.find()) {
                    var thumbNailUrl =
                        streamMap.substring(thumbnailMatcher.start(), thumbnailMatcher.end())
                    thumbNailUrl = thumbNailUrl.replace("<img class=\"_3chq\" src=\"", "")
                        .replace("\" />", "").replace("&amp;", "&")
                    Log.e("Extractor", "Thumbnail :: NULL")
                    Log.e("Extractor", "Thumbnail :: $thumbNailUrl")
                    facebookFile?.thumbNailUrl = URLDecoder.decode(thumbNailUrl, "UTF-8")
                }

            }
            if (hdVideoMatcher.find()) {
                var vUrl1 = hdVideoMatcher.group()
                vUrl1 = vUrl1.substring(8, vUrl1.length - 1) //hd_scr: 8 char
                facebookFile?.hdUrl = vUrl1

                if (showLogs) {
                    Log.e("Extractor", "HD_URL :: Null")
                    Log.e("Extractor", "HD_URL :: $vUrl1")
                }

            } else {
                facebookFile?.hdUrl = null
            }
            if (imageMatcher.find()) {
                var imageUrl =
                    streamMap.substring(imageMatcher.start(), imageMatcher.end())
                imageUrl = imageUrl.replace("<meta property=\"og:image\" content=\"", "")
                    .replace("\" />", "").replace("&amp;", "&")
                Log.e("Extractor", "FILENAME :: NULL")
                Log.e("Extractor", "FILENAME :: $imageUrl")
                facebookFile?.imageUrl = URLDecoder.decode(imageUrl, "UTF-8")
            }
            if (facebookFile?.sdUrl == null && facebookFile?.hdUrl == null) {
            }
            loaded(facebookFile!!)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

我想实现一个功能,我可以显示不同的分辨率和尺寸,如图所示。

请注意,我已经用 HD URL 的视频测试了我的 linkParsing 方法,但它只提供 SD URL。

这是一个示例视频 link:https://fb.watch/aENyxV7gxs/

如何做到这一点?我无法为此找到任何合适的方法或 GitHub 库。

已找到解决方案,因此发布为答案。

这可以通过提取网页的 Page Source 然后解析该 XML 并获取 BASE URL 列表来完成。

步骤如下:

1- 在 Webview 中加载特定视频 URL 并在 onPageFinished

中获取页面源代码
private fun webViewSetupNotLoggedIn() {
    webView?.settings?.javaScriptEnabled = true
    webView?.settings?.userAgentString = AppConstants.USER_AGENT
    webView?.settings?.useWideViewPort = true
    webView?.settings?.loadWithOverviewMode = true
    webView?.addJavascriptInterface(this, "mJava")
    webView?.post {
        run {
            webView?.loadUrl(“url of your video")
        }
    }
    object : WebViewClient() {
        override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
            if (url == "https://m.facebook.com/login.php" || url.contains("https://m.facebook.com/login.php")
            ) {
                webView?.loadUrl("url of your video")
            }
            return true
        }

        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
        }
    }
    webView.webChromeClient = object : WebChromeClient() {
        override fun onProgressChanged(view: WebView?, newProgress: Int) {
            super.onProgressChanged(view, newProgress)
            if (progressBarBottomSheet != null) {
                if (newProgress == 100) {
                    progressBarBottomSheet.visibility = View.GONE
                } else {
                    progressBarBottomSheet.visibility = View.VISIBLE
                }
                progressBarBottomSheet.progress = newProgress
            }
        }
    }
    webView?.webViewClient = object : WebViewClient() {
        override fun onPageFinished(view: WebView?, url: String?) {
            try {
                if (webView?.progress == 100) {
                    var original = webView?.originalUrl
                    var post_link = "url of your video"
                    if (original.equals(post_link)) {
                        var listOfResolutions = arrayListOf<ResolutionDetail>()
                        val progressDialog = activity?.getProgressDialog(false)
                        progressDialog?.show()

                        //Fetch resoultions
                        webView.evaluateJavascript(
                            "(function(){return window.document.body.outerHTML})();"
                        ) { value ->
                            val reader = JsonReader(StringReader(value))
                            reader.isLenient = true
                            try {
                                if (reader.peek() == JsonToken.STRING) {
                                    val domStr = reader.nextString()
                                    domStr?.let {
                                        val xmlString = it
                                        CoroutineScope(Dispatchers.Main).launch {
                                            CoroutineScope(Dispatchers.IO).async {
                                                try {
                                                    getVideoResolutionsFromPageSource((xmlString)) {
                                                        listOfResolutions = it
                                                    }
                                                } catch (e: java.lang.Exception) {
                                                    e.printStackTrace()
                                                    Log.e("Exception", e.message!!)
                                                }
                                            }.await()
                                            progressDialog?.hide()
                                            if (listOfResolutions.size > 0) {
                                                setupResolutionsListDialog(listOfResolutions)
                                            } else {
                                                Toast.makeText(
                                                    context,
                                                    "No Resolutions Found",
                                                    Toast.LENGTH_SHORT
                                                ).show()
                                            }
                                        }
                                    }
                                }
                            } catch (e: IOException) {
                                e.printStackTrace()
                            } finally {
                                reader.close()
                            }
                        }
                    }
                }
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
            super.onPageFinished(view, url)
        }

        @TargetApi(android.os.Build.VERSION_CODES.M)
        override fun onReceivedError(
            view: WebView?,
            request: WebResourceRequest?,
            error: WebResourceError
        ) {

        }

        @SuppressWarnings("deprecation")
        override fun onReceivedError(
            view: WebView?,
            errorCode: Int,
            description: String?,
            failingUrl: String?
        ) {
            super.onReceivedError(view, errorCode, description, failingUrl)
        }

        override fun onLoadResource(view: WebView?, url: String?) {
            Log.e("getData", "onLoadResource")

            super.onLoadResource(view, url)
        }
    }
}

2- 获取页面源时解析以获取视频 解析 URL

fun getVideoResolutionsFromPageSource(
    pageSourceXmlString: String?,
    finished: (listOfRes: ArrayList<ResolutionDetail>) -> Unit
) {
    //pageSourceXmlString is the Page Source of WebPage of that specific copied video
    //We need to find list of Base URLs from pageSourceXmlString
    //Base URLs are inside an attribute named data-store which is inside a div whose class name starts with  '_53mw;
    //We need to find that div then get data-store which has a JSON as string
    //Parse that JSON and we will get list of adaptationset
    //Each adaptationset has list of representation tags
    // representation is the actual div which contains BASE URLs
    //Note that: BASE URLs have a specific attribute called mimeType
    //mimeType has audio/mp4 and video/mp4 which helps us to figure out whether the url is of an audio or a video
    val listOfResolutions = arrayListOf<ResolutionDetail>()
    if (!pageSourceXmlString?.isEmpty()!!) {
        val document: org.jsoup.nodes.Document = Jsoup.parse(pageSourceXmlString)
        val sampleDiv = document.getElementsByTag("body")
        if (!sampleDiv.isEmpty()) {
            val bodyDocument: org.jsoup.nodes.Document = Jsoup.parse(sampleDiv.html())
            val dataStoreDiv: org.jsoup.nodes.Element? = bodyDocument.select("div._53mw").first()
            val dataStoreAttr = dataStoreDiv?.attr("data-store")
            val jsonObject = JSONObject(dataStoreAttr)
            if (jsonObject.has("dashManifest")) {
                val dashManifestString: String = jsonObject.getString("dashManifest")
                val dashManifestDoc: org.jsoup.nodes.Document = Jsoup.parse(dashManifestString)
                val mdpTagVal = dashManifestDoc.getElementsByTag("MPD")
                val mdpDoc: org.jsoup.nodes.Document = Jsoup.parse(mdpTagVal.html())
                val periodTagVal = mdpDoc.getElementsByTag("Period")
                val periodDocument: org.jsoup.nodes.Document = Jsoup.parse(periodTagVal.html())
                val subBodyDiv: org.jsoup.nodes.Element? = periodDocument.select("body").first()
                subBodyDiv?.children()?.forEach {
                    val adaptionSetDiv: org.jsoup.nodes.Element? =
                        it.select("adaptationset").first()
                    adaptionSetDiv?.children()?.forEach {
                        if (it is org.jsoup.nodes.Element) {
                            val representationDiv: org.jsoup.nodes.Element? =
                                it.select("representation").first()
                            val resolutionDetail = ResolutionDetail()
                            if (representationDiv?.hasAttr("mimetype")!!) {
                                resolutionDetail.mimetype = representationDiv?.attr("mimetype")
                            }
                            if (representationDiv?.hasAttr("width")!!) {
                                resolutionDetail.width =
                                    representationDiv?.attr("width")?.toLong()!!
                            }
                            if (representationDiv?.hasAttr("height")!!) {
                                resolutionDetail.height =
                                    representationDiv.attr("height").toLong()
                            }
                            if (representationDiv?.hasAttr("FBDefaultQuality")!!) {
                                resolutionDetail.FBDefaultQuality =
                                    representationDiv.attr("FBDefaultQuality")
                            }
                            if (representationDiv?.hasAttr("FBQualityClass")!!) {
                                resolutionDetail.FBQualityClass =
                                    representationDiv.attr("FBQualityClass")
                            }
                            if (representationDiv?.hasAttr("FBQualityLabel")!!) {
                                resolutionDetail.FBQualityLabel =
                                    representationDiv.attr("FBQualityLabel")
                            }
                            val representationDoc: org.jsoup.nodes.Document =
                                Jsoup.parse(representationDiv.html())
                            val baseUrlTag = representationDoc.getElementsByTag("BaseURL")
                            if (!baseUrlTag.isEmpty() && !resolutionDetail.FBQualityLabel.equals(
                                    "Source",
                                    ignoreCase = true
                                )
                            ) {
                                resolutionDetail.videoQualityURL = baseUrlTag[0].text()
                                listOfResolutions.add(resolutionDetail)
                            }
                        }
                    }
                }
            }
        }
    }
    finished(listOfResolutions)
}

class ResolutionDetail {
    var width: Long = 0
    var height: Long = 0
    var FBQualityLabel = ""
    var FBDefaultQuality = ""
    var FBQualityClass = ""
    var videoQualityURL = ""
    var mimetype = ""  // [audio/mp4 for audios and video/mp4 for videos]
}

3- 将 videoQualityURL 传递给您的视频下载功能,将下载所选分辨率的视频。