2020/8/11 停止維護
hichannel 更改了 token ,現在只需要一個 token 和一個 expires ,因為沒有實際利益 Hoyo 本人也沒在使用這個收聽功能,因此停止維護
--
實際運作結果
--
先看一下可播放的實際案例網址
1 |
http://radio-hichannel.cdn.hinet.net/live/pool/hich-ra000073/ra-hls/index.m3u8?token1=ICzRSfp8Qy6wVuIDVIlOLg&token2=cSVWYP2kBp49hiIha41Y-A&expire1=1462880161&expire2=1462908961 |
解讀
- ra-000073 是頻道的代號名稱,ra-000073 是 NEWS98 ,KissRadio 是 ra-000042
- index.m3u8 表示使用 HTTP HLS 串流技術
- expire1 & expire2 表示可以執行播放的時間區間,你拿一個早上八點到下午四點的播放網址在晚上執行是不能播放的
- token1 & token2 和 expire 是相呼應的,意思就是如果 expire 的數值為 1000 算出 token 是 ABC999 ,那一樣的執行環境算出的 token 就會相同
--
參考資料
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#!/usr/bin/env python import time import base64 import hashlib import urllib import urlparse import urllib2 import subprocess from collections import OrderedDict def gen_token(path, timestamp, ip_addr, token_ord): const_str = 'radio@himediaservice#t' cat_str = path + str(timestamp) + ip_addr + const_str + str(token_ord) hashed = hashlib.md5(unicode(cat_str)).digest() b64_md5hash = base64.b64encode(hashed) return b64_md5hash.replace('+', '-').replace('/', '_').replace('=', '') def build_url(): base_url = 'http://radio-hichannel.cdn.hinet.net/' ip_addr = urllib2.urlopen('http://ipinfo.io/ip').read().rstrip() path = '/live/pool/hich-ra000009/ra-hls/' expire1 = int(time.time()) expire2 = expire1 + (60 * 60 * 8) params = OrderedDict([ ('token1', gen_token(path, expire1, ip_addr, 1)), ('token2', gen_token(path, expire2, ip_addr, 2)), ('expire1', expire1), ('expire2', expire2) ]) params_str = urllib.urlencode(params) return urlparse.urljoin(base_url, path + 'index.m3u8') + '?' + params_str def check_url_alive(url): try: urllib2.urlopen(url) return True except urllib2.HTTPError: return False def ffplay(source): _prefix_url = "ffmpeg://{}".format(source) print "HIT RETURN TO QUIT\n" subprocess.call(['mplayer', '-msglevel', 'all=-1', '-cache', '1024', _prefix_url]) def main(): is_alive = None while is_alive != True: url = build_url() is_alive = check_url_alive(url) try: ffplay(url) except KeyboardInterrupt: exit() if __name__ == '__main__': main() |
寫 PHP 的人轉到 python 會遇到的問題有
- 結束不用加分號
- \t 和空白是坑爹的不一樣
- 縮行不是爽縮就可以縮
- 相同功能的 function 預設的輸出和 PHP 不同
好好研究一下網頁是如何運作,追到了 radio_proxy.swf 這個檔案,網頁的播放最後就是由這個 Flash 檔案生成播放路徑,這時又要尋找「反組譯」黑科技
可惜的是,閃客 Sothink SWF Decompile 解不開,大概也有反制黑科技的技術吧
還好這種東西有線上版可以用!?...
不知道當時哪來的腦洞想到上網找 online 版,不過結果是美好的,至少是解開有看到類似 ActionScript 的輸出了。
下面是有關產生 token 的片段程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public function buildPlayUrl(clientIP:String, url:String):String{ if ((clientIP == null)){ } else { (clientIP == null); }; if ((clientIP == null)){ return ("null ip or url"); }; var pos:int = url.lastIndexOf("/"); if (pos >= 0){ } else { throw (new ArgumentError(("error stream url format:" + url))); }; this.debug(("build token => client ip:" + clientIP)); var len:int = url.length; var location:String = url.substr((pos + 1), len).toLowerCase(); var path:String = (("/live/pool/hich-" + location) + "/ra-hls/"); var now:Date = new Date(); now.time = (now.time + ((1000 * 60) * 5)); var expire1:String = this.toSecTimeStr(now.time); now.time = (now.time + (((1000 * 60) * 60) * 8)); var expire2:String = this.toSecTimeStr(now.time); var key1:String = (this.KEYS[0] + "1"); var key2:String = (this.KEYS[0] + "2"); var token1:String = this.buildToken(path, expire1.toString(), clientIP, key1); var token2:String = this.buildToken(path, expire2.toString(), clientIP, key2); var mp4Url:String = ((this.DOMAIN[0] + path) + "index.m3u8"); return (StringUtil.substitute("{0}?token1={1}&token2={2}&expire1={3}&expire2={4}", mp4Url, token1, token2, expire1.toString(), expire2.toString())); } private function toSecTimeStr(timeMillis:Number):String{ var secs:Number = int((timeMillis / 1000)); return (secs.toString()); } private function buildToken(path:String, expire:String, ip:String, key:String):String{ var sec:String = (((path + expire) + ip) + key); var bytes:ByteArray = new ByteArray(); bytes.writeUTFBytes(sec); var md5:MD5 = new MD5(); var hash:ByteArray = md5.hash(bytes); var token:String = Base64.encodeByteArray(hash); var pat1:RegExp = /\+/gi; var pat2:RegExp = /\//gi; var pat3:RegExp = /=/gi; token = token.replace(pat1, "-").replace(pat2, "_").replace(pat3, ""); return (token); } |
基本上和 python 架構是一樣的,只是反組譯有少東西,最關鍵的 KEYS 沒有解開,只好回頭繼續研究 python
後來證實 python 有關 token 的演算法是正確的
--
Token 演算法
token1 字串組成
1 |
path + expire timestamp + IP + Key + 1 |
token2 字串組成
1 |
path + expire timestamp + IP + Key + 2 |
token1 範例,token2 只要將最後的 1 改成 2 即可
1 |
/live/pool/hich-ra000073/ra-hls/146295177759.125.207.208radio@himediaservice#t1 |
然後拿去 md5() 再跑 base64_encode() 就可以得到 Token 了
--
index.m3u8 內容
1 2 3 4 5 6 |
http://radio-hichannel.cdn.hinet.net/live/pool/hich-ra000036/ra-hls/index.m3u8?token1=IKGH3KBN_Fy5_mAvsgaibQ&token2=Af9RZuEDZ_yK9TIBmIuFTw&expire1=1462943429&expire2=1462972229 #EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=135680,CODECS="avc1.100.30,mp4a.40.2",RESOLUTION=362x242 hich-ra000036-video=0-audio=128000.m3u8?token1=IKGH3KBN_Fy5_mAvsgaibQ&token2=Af9RZuEDZ_yK9TIBmIuFTw&expire1=1462943429&expire2=1462972229 |
--
PHP 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php header("Content-Type: text/html; charset=utf-8"); date_default_timezone_set("Asia/Taipei"); set_time_limit(0); ini_set('error_reporting',E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED); function gen_token( $path, $timestamp, $ip_addr, $token_ord ) { $const_str = 'radio@himediaservice#t'; $cat_str = $path . $timestamp . $ip_addr . $const_str . $token_ord; $hashed = md5($cat_str, true); $b64_md5hash = base64_encode($hashed); return str_replace( '=', '', str_replace( '/', '_', str_replace( '+', '-', $b64_md5hash))); } $path = '/live/pool/hich-ra000018/ra-hls/'; $expire1 = time()+ (60 * 60 * 2); //$expire1 = 1462951777; $expire2 = $expire1 + (60 * 60 * 12); //$expire2 = 1462980577; $ip = $_SERVER['REMOTE_ADDR']; // 執行環境對外的 IP $token1 = gen_token($path, $expire1, $ip, 1); $token2 = gen_token($path, $expire2, $ip, 2); // $m3u8 就是播放路徑 $m3u8 = 'http://radio-hichannel.cdn.hinet.net/live/pool/hich-ra000018/ra-hls/index.m3u8?token1='. $token1 .'&token2=' . $token2 . '&expire1='. $expire1 .'&expire2='. $expire2; //echo $m3u8; $Filename = date('YmdHi00'); exec('/usr/bin/ffmpeg -i "'. $m3u8 .'" -t 01:00:00 /tmp/mp3/'. $Filename .'.mp3 &'); |
--
更新
- 2019-02-20 網址改為 https:// 以及又改為 index.m3u8 的檔名
- 2018-06-29 最後 m3u8 產出時, index.m3u8 檔案名稱必須改成 ra-hls/hich-ra000073-audio_track=128000.m3u8 這種格式的檔案名稱
--
9,114 total views, 2 views today
您好,
請問path + expire timestamp + IP + Key + 1/2, 其中的key value是怎麼來的?constant?
後面1/2又是什麼意思?在範例中的字串內容, 最後的#t1好像又跟1/2沒什麼關聯.
謝謝
Key 必須從 Flash swf 反組譯得到,hoyo 沒得到,因為反不出來 ...
那個 1/2 是當初省略亂寫的,代表 token1 和 token2 ,已經把文件修改成更容易閱讀了。
你好
想請問一下
有辦法追到前幾天的紀錄
把之前的內容載下來嗎
除非有提供相關的服務或選項,要不然是不可能的。
真的喜歡該節目不妨直接和節目組聯絡,說不定會有額外驚喜呦... (瞎唬爛的)
這個方法是不是又被擋了
QQ
沒有喔,剛剛 (2017-09-14 13:44) 測試還是可以的
那個 KEYS "據說"是某 standard 中的 test vector key
hichannel 搞這一大圈,是想要留大家在他網頁上賺廣告費嗎 XDD
請問這個方法還有用嗎?我 run 了但是抓出來的 url 都是顯示 403 forbidden.
有用,不過最後的 m3u8 檔案名稱有更改,內容已經更新
改天再寫一個 API 直接提供收聽網址好了
謝謝大大分享,確實可以用.
另外 mplayer 記得要安裝.
我直接執行您的 php code, 結果403 forbidden
有網址可以看嗎?
如果只是想要測試了解,請直接使用 https://hoyo.idv.tw/?a=Tools/HinetRadio 取得播放路徑使用即可
你好,有辦法從https://hichannel.hinet.net/radio/index.do 取得某個廣播的串流ip嗎?
謝謝
我不會,或者是不可能會知道
試想,今天你直播到 youtube ,youtube 如果不特別提供,有辦法知道你推流的 ip 嗎?
你好,在ubuntu16可否用ffplay播放呢?使用您程式中ffmpeg指令播放出現 No trailing CRLF found in HTTP header,加上header參數後沒有錯誤訊息但沒有聲音,一樣環境使用ffplay播放mp3有聲音,可能還要甚麼參數嗎?
謝謝
Hoyo 沒那麼熱血用 Linux 來當作主力機,不用過 Windows 的 ffplay 播放 Hinet 的 m3u8 也是會有錯誤,不過是 SDL 的問題不是 HTTP Header 網路方面的
有可能是連結網址需要更新,剛剛把提供網址的小工具更新了一下,請用新網址再試試看
https://hoyo.idv.tw/?a=Tools/HinetRadio
python3.5+我做了一些修正就可以執行,不過,瑕不掩瑜,it's working perfectly !
def myIP():
resp=urllib.request.urlopen('http://ipinfo.io/ip')
html=resp.read()
return html.strip().decode()
def gen_token(path, timestamp, ip_addr, token_ord):
const_str = 'radio@himediaservice#t'
cat_str = path + str(timestamp) + ip_addr + const_str + str(token_ord)
hashed = hashlib.md5(cat_str.encode(encoding='utf-8')).digest()
b64_md5hash = base64.b64encode(hashed)
return b64_md5hash.decode().replace('+', '-').replace('/', '_').replace('=', '')
def build_radio_url(radioid="ra000040"):
base_url = 'https://radio-hichannel.cdn.hinet.net/'
ip_addr = myIP()
path = '/live/pool/hich-{0}/ra-hls/'.format(radioid)
expire1 = int(time.time())
expire2 = expire1 + (60 * 60 * 8)
params = OrderedDict([
('token1', gen_token(path, expire1, ip_addr, 1)),
('token2', gen_token(path, expire2, ip_addr, 2)),
('expire1', expire1),
('expire2', expire2)
])
params_str = urllib.parse.urlencode(params)
return urllib.parse.urljoin(base_url, path + 'hich-{0}-audio_track=128000.m3u8'.format(radioid)) + '?' + params_str
貓爸,可能是 WP 回覆有限制,所以程式碼被裁切,格式也跑掉了
如果有網友有詢問到 py 3.5 的需求我再讓網友去問貓爸好了 哈哈哈
来自洛杉矶的你好。 对不起,我的中文很糟糕。 我可以获得频道9,但频道73(news98)不再有效。 我想知道为什么?
hinet new98 收聽會抽風,以下是官方說明
http://www.news98.com.tw/cdn-stream.asp#.WK23R2997IV
可以直接使用 http://cast.news98.com.tw:8000/news98 收聽
非常感谢你。 news98和ufo现在都很容易下载。
現在是否又有問題 ??
是的,沒錯,現在改成 https://hiradio-hichannel.cdn.hinet.net/live/pool/hich-ra000001/ra-hls/index.m3u8?token=4S-QT5V_cFLAogZnJcZk0A&expires=1597202971 這樣
和原來差了很多,沒有好處所以就維護到這裡,回家把連結移除吧 :)