【虎牙直播源】浏览器抓取真实直播源地址(纯前端JS解析源码)

浏览器抓取真实直播源地址(纯前端JS解析)

网上搜索各种平台的直播源地址都是满天飞,但是经常会有失效的时候,因为官方也会定期的升级系统修改各种参数或链接让直播源不能永久,所以敝人一直崇尚的是 授人以鱼不如授人以渔,与其给直播源别人,不如教大家如何去爬取直播源,就算失效了也不怕。
在这里插入图片描述

0. 前言

本人业余时间喜欢用虎牙看直播,所以第一个便是想到如何抓取虎牙的直播源。
在抓取之前,需要了解视频直播源的分类和区别,可以自行了解hls,flv,m3u8等知识。

Tips: 本教程只是教大家如何利用前端调试技巧和爬虫基本操作,不作为商业用途,各位童鞋耗子尾汁。

1. 浏览器抓取流程

首选打开虎牙官网,随便找个直播间:https://m.huya.com/949527,这里是使用的手机端的网页(因为手机端的简单)
随便看了下,没有ajax请求,那么地址定是随页面带进来了,现在大部分直播网页都是SSR(服务器端渲染),所以只能去页面源代码找找:
好家伙!直接就找到了一个很想地址的东西 liveLineUrl,是一个m3u8的地址:
在这里插入图片描述

在线m3u8播放测试网站:https://www.m3u8play.com/

这个网站可以测试播放源是不是好的,来!试一下!
在这里插入图片描述
就很完美!
在这里插入图片描述
但是就这么简单的吗?
在这里插入图片描述
我又试了一下我经常看的【一起看】的直播间,来看看电影啥的,结果:
在这里插入图片描述
在这里插入图片描述
这是咋回事。。。然后对比下前后两个链接发现了问题,下面是【一起看】的链接:
在这里插入图片描述
然后想到 liveLineUrl 这个参数不是全局变量吗,控制台打印看一下,再仔细对比发现参数变了有个fm参数已经变成了seqid
在这里插入图片描述
先试下控制台打印的能不能播放:
在这里插入图片描述
行,司马懿出来了,现在只用分析如何破解参数即可。

2. 参数解析

Ctrl + Shift + F 搜索 liveLineUrl , 然后找到这里处理urljs,打个断点调试一下,看看怎么处理的:
在这里插入图片描述
断点进入 Object(m.default)(window.liveLineUrl) 可以看到这里就是处理参数的地方,最后返回的就是解析后的参数字符串:
在这里插入图片描述
我整理了下解析函数,重新实现了一下:

function parseUrl(url){
    let params = url.split("?")[1];
    params = params.split("&");
    let paramsObj = {};
    for (let i = 0; i < params.length; i++) {
        let item = params[i].split("=");
        2 === item.length && (paramsObj[item[0]] = item[1])
    }

    let mainUrl = url.split("?")[0];
    let r = mainUrl.split("/");
    let streamName = r[r.length - 1].replace(/.(flv|m3u8)/g, "");
    let {fm: fm, wsTime: wsTime, wsSecret: u, ...others} = paramsObj;
    let fmParse = Base64.decode(decodeURIComponent(fm));
    let p = fmParse.split("_")[0];
    let time = parseInt(1e4 * (new Date).getTime() + 1e4 * Math.random());
    let newWsSecret = `${p}_0_${streamName}_${time}_${wsTime}`;
    newWsSecret = md5.hex(newWsSecret);
    let y = "";
    Object.keys(others).forEach(e=>{
        y += `&${e}=${others[e]}`
    });
    return `${mainUrl}?wsSecret=${newWsSecret}&wsTime=${wsTime}&u=0&seqid=${time}${y}`;
}

其中用到了Base64MD5相关函数:

// md5下载:https://raw.githubusercontent.com/emn178/js-md5/master/src/md5.js

let Base64 = {
    _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
    encode: function(e) {
        var t = "";
        var n, r, i, s, o, u, a;
        var f = 0;
        e = Base64._utf8_encode(e);
        while (f < e.length) {
            n = e.charCodeAt(f++);
            r = e.charCodeAt(f++);
            i = e.charCodeAt(f++);
            s = n >> 2;
            o = (n & 3) << 4 | r >> 4;
            u = (r & 15) << 2 | i >> 6;
            a = i & 63;
            if (isNaN(r)) {
                u = a = 64
            } else if (isNaN(i)) {
                a = 64
            }
            t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
        }
        return t
    },
    decode: function(e) {
        var t = "";
        var n, r, i;
        var s, o, u, a;
        var f = 0;
        e = e.replace(/[^A-Za-z0-9+/=]/g, "");
        while (f < e.length) {
            s = this._keyStr.indexOf(e.charAt(f++));
            o = this._keyStr.indexOf(e.charAt(f++));
            u = this._keyStr.indexOf(e.charAt(f++));
            a = this._keyStr.indexOf(e.charAt(f++));
            n = s << 2 | o >> 4;
            r = (o & 15) << 4 | u >> 2;
            i = (u & 3) << 6 | a;
            t = t + String.fromCharCode(n);
            if (u != 64) {
                t = t + String.fromCharCode(r)
            }
            if (a != 64) {
                t = t + String.fromCharCode(i)
            }
        }
        t = Base64._utf8_decode(t);
        return t
    },
    _utf8_encode: function(e) {
        e = e.replace(/rn/g, "n");
        var t = "";
        for (var n = 0; n < e.length; n++) {
            var r = e.charCodeAt(n);
            if (r < 128) {
                t += String.fromCharCode(r)
            } else if (r > 127 && r < 2048) {
                t += String.fromCharCode(r >> 6 | 192);
                t += String.fromCharCode(r & 63 | 128)
            } else {
                t += String.fromCharCode(r >> 12 | 224);
                t += String.fromCharCode(r >> 6 & 63 | 128);
                t += String.fromCharCode(r & 63 | 128)
            }
        }
        return t
    },
    _utf8_decode: function(e) {
        var t = "";
        var n = 0;
        var r = c1 = c2 = 0;
        while (n < e.length) {
            r = e.charCodeAt(n);
            if (r < 128) {
                t += String.fromCharCode(r);
                n++
            } else if (r > 191 && r < 224) {
                c2 = e.charCodeAt(n + 1);
                t += String.fromCharCode((r & 31) << 6 | c2 & 63);
                n += 2
            } else {
                c2 = e.charCodeAt(n + 1);
                c3 = e.charCodeAt(n + 2);
                t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
                n += 3
            }
        }
        return t
    }
}

3. 源码及播放器实现

播放器官网: videojs

来吧,直接上全部代码:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="applicable-device" content="pc,mobile">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/video.js@7.4.1/dist/video-js.min.css">
    <title>虎牙直播</title>
</head>
<body>
    <video id="player" class="video-js vjs-16-9 vjs-big-play-centered" controls preload="auto"  data-setup="{}">
        <source src="https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8" type="application/x-mpegURL">
        <p class="vjs-no-js">
            To view this video please enable JavaScript, and consider upgrading to a web browser that
            <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
        </p>
    </video>
    <script src="https://cdn.jsdelivr.net/npm/video.js@7.4.1/dist/video.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@videojs/http-streaming@1.10.3/dist/videojs-http-streaming.min.js"></script>
    <script src="js/md5.js"></script>
    <script>
        let Base64 = {
            _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
            encode: function(e) {
                var t = "";
                var n, r, i, s, o, u, a;
                var f = 0;
                e = Base64._utf8_encode(e);
                while (f < e.length) {
                    n = e.charCodeAt(f++);
                    r = e.charCodeAt(f++);
                    i = e.charCodeAt(f++);
                    s = n >> 2;
                    o = (n & 3) << 4 | r >> 4;
                    u = (r & 15) << 2 | i >> 6;
                    a = i & 63;
                    if (isNaN(r)) {
                        u = a = 64
                    } else if (isNaN(i)) {
                        a = 64
                    }
                    t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
                }
                return t
            },
            decode: function(e) {
                var t = "";
                var n, r, i;
                var s, o, u, a;
                var f = 0;
                e = e.replace(/[^A-Za-z0-9+/=]/g, "");
                while (f < e.length) {
                    s = this._keyStr.indexOf(e.charAt(f++));
                    o = this._keyStr.indexOf(e.charAt(f++));
                    u = this._keyStr.indexOf(e.charAt(f++));
                    a = this._keyStr.indexOf(e.charAt(f++));
                    n = s << 2 | o >> 4;
                    r = (o & 15) << 4 | u >> 2;
                    i = (u & 3) << 6 | a;
                    t = t + String.fromCharCode(n);
                    if (u != 64) {
                        t = t + String.fromCharCode(r)
                    }
                    if (a != 64) {
                        t = t + String.fromCharCode(i)
                    }
                }
                t = Base64._utf8_decode(t);
                return t
            },
            _utf8_encode: function(e) {
                e = e.replace(/rn/g, "n");
                var t = "";
                for (var n = 0; n < e.length; n++) {
                    var r = e.charCodeAt(n);
                    if (r < 128) {
                        t += String.fromCharCode(r)
                    } else if (r > 127 && r < 2048) {
                        t += String.fromCharCode(r >> 6 | 192);
                        t += String.fromCharCode(r & 63 | 128)
                    } else {
                        t += String.fromCharCode(r >> 12 | 224);
                        t += String.fromCharCode(r >> 6 & 63 | 128);
                        t += String.fromCharCode(r & 63 | 128)
                    }
                }
                return t
            },
            _utf8_decode: function(e) {
                var t = "";
                var n = 0;
                var r = c1 = c2 = 0;
                while (n < e.length) {
                    r = e.charCodeAt(n);
                    if (r < 128) {
                        t += String.fromCharCode(r);
                        n++
                    } else if (r > 191 && r < 224) {
                        c2 = e.charCodeAt(n + 1);
                        t += String.fromCharCode((r & 31) << 6 | c2 & 63);
                        n += 2
                    } else {
                        c2 = e.charCodeAt(n + 1);
                        c3 = e.charCodeAt(n + 2);
                        t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
                        n += 3
                    }
                }
                return t
            }
        }

        function parseUrl(url){
            let params = url.split("?")[1];
            params = params.split("&");
            let paramsObj = {};
            for (let i = 0; i < params.length; i++) {
                let item = params[i].split("=");
                2 === item.length && (paramsObj[item[0]] = item[1])
            }

            let mainUrl = url.split("?")[0];
            let r = mainUrl.split("/");
            let streamName = r[r.length - 1].replace(/.(flv|m3u8)/g, "");
            let {fm: fm, wsTime: wsTime, wsSecret: u, ...others} = paramsObj;
            let fmParse = Base64.decode(decodeURIComponent(fm));
            let p = fmParse.split("_")[0];
            let time = parseInt(1e4 * (new Date).getTime() + 1e4 * Math.random());
            let newWsSecret = `${p}_0_${streamName}_${time}_${wsTime}`;
            newWsSecret = md5.hex(newWsSecret);
            let y = "";
            Object.keys(others).forEach(e=>{
                y += `&${e}=${others[e]}`
            });
            return `${mainUrl}?wsSecret=${newWsSecret}&wsTime=${wsTime}&u=0&seqid=${time}${y}`;
        }

        let e = parseUrl("//al.hls.huya.com/src/1423787831-1423787831-6115122170587774976-2847699118-10057-A-0-1-imgplus_2000.m3u8?wsSecret=f9aaf4fcbe42e724d152c265cf1837fb&wsTime=5ff71b32&fm=RFdxOEJjSjNoNkRKdDZUWV8kMF8kMV8kMl8kMw%3D%3D&ctype=tars_mobile&txyp=o%3Aj10%3B&fs=bgct&&sphdcdn=al_7-tx_3-js_3-ws_7-bd_2-hw_2&sphdDC=huya&sphd=264_*-265_*&t=103");
        let t = videojs("#player");
        t.src(e);
        t.play();
    </script>
</body>
</html>

看看诸葛亮弹琴退仲达 :
在这里插入图片描述

4. 总结

  • 目前发现虎牙【一起看】栏目下的需要把url做第二次解析,普通直播间可以直接拿来播放;
  • 有人可能说为啥要用移动端的页面解析,而不用PC端的,其实也可以的,分析源码可以找到一个config对象,里面就包含了所需的信息,看到这些参数熟悉不?(同样的【一起看】栏目的需要二次解析):
    在这里插入图片描述

在这里插入图片描述

  • 19
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
1. 哪吒 2. 无情的游戏 3. 逆战 4. 荒野行动 5. 绝地求生 6. 阴阳师 7. 征途2 8. 最强王者 9. 格斗之王 10. 王者荣耀 11. 穿越火线 12. 变形金刚 13. 刺激战场 14. 逆水寒 15. 明日之后 16. 街头篮球 17. 小小军团 18. 风暴英雄 19. 镇魂街 20. 皇室战争 21. 神武4 22. 神龙猎手 23. 三国杀 24. 战争雷霆 25. 天涯明月刀 26. 猎魔群英传 27. 剑网3 28. 天龙八部 29. 火影忍者 30. 小米枪战 31. 大话西游2 32. 守望先锋 33. 问道 34. 诛仙3 35. 神鬼传奇 36. 魔兽世界 37. 真三国无双 38. 赛尔号 39. 命运2 40. 龙之谷 41. 大天使之剑 42. 劲舞团 43. 热血江湖 44. 神仙道 45. 使命召唤 46. 洛奇英雄传 47. 梦幻西游 48. 钢铁雄心 49. 光荣使命 50. 大话封神 51. 永恒之塔 52. 塔防三国志 53. 魔域 54. DNF 55. 神话 56. 奇迹MU 57. 神雕侠侣 58. 火影忍者OL 59. 奇迹世界 60. 热血传奇 61. 鬼泣 62. 王牌战士 63. 传奇霸业 64. 十万个冷笑话 65. 传世之爱 66. 风暴战区 67. 御龙在天 68. 乱斗西游 69. 九阴真经 70. 九州缥缈录 71. 一起来捉妖 72. 诸神之战 73. 飞车 74. 突围行动 75. 乱世王者 76. 梦幻诛仙 77. 梦想世界 78. 洛克王国 79. 问道九州 80. 掌上英雄联盟 81. 梦幻西游手游 82. LOL 83. 神雕侠侣手游 84. 问道手游 85. 天龙八部手游 86. 神武4手游 87. 逆水寒手游 88. 大话西游手游 89. 火影忍者手游 90. 格斗之王手游 91. 进击的巨人手游 92. 剑网3手游 93. 王者荣耀手游 94. 神龙猎手手游 95. 小米枪战手游 96. 穿越火线手游 97. 绝地求生手游 98. 刺激战场手游 99. 决战!平安京 100. 皇室战争手游

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

优小U

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值