本系列文章是針對 https://blog.csdn.net/weixin_43668031/article/details/83962959 內容的實現所編寫的。開發經歷包括思考過程、重構和推翻重來。
【IPFS直播】 利用ipfs協議傳輸進行直播
1.實驗環境以及工具介紹
系統:Ubuntu 18.04.2
IPFS:go-ipfs v0.4.19
Nginx:源碼編譯安裝(添加nginx-rtmp-module)
ffmpeg:通過apt安裝
監視腳本語言:Python
2.基本邏輯架構
去年我思考了如何利用ipfs技術進行直播,構思如下圖:
核心部分是流媒體傳輸,這裏是使用HLS分片成ts小文件進行傳輸的,本節僅完成核心邏輯部分的實現代碼。
這裏要着重聲明一下,Live Server 這個角色,在目前的技術背景下,這必須是個Nginx服務器,但是在未來可以和直播推流端合併在一起。就像近幾年家庭入網時的光貓合併進路由器一樣,“家庭網關”=光纖調制解調器+路由器+交換機+無線交換機,一臺設備集成了不同的傳統設備,就算在視頻直播設備上面,最原始傳統的採集卡只能輸出原始視頻未壓縮的視頻,隨着其他模塊集成,帶有x264壓縮模塊的採集卡就誕生了,可以直接輸出x264格式的視頻流、文件,隨着直播行業的興起,帶有推流功能的採集卡也單身了,把平臺推流相關的配置導入到其中,就可以進行推流了,我相信不久就有包含上述架構的採集卡(硬件)或者打包成一體的軟件突出。
在瀏覽器裏運行ipfs節點時,我無法接受、推送來着非直連節點的信息(貌似沒有轉發?),也許是因爲性能問題,無法處理過大廣播數據,所以只能用瀏覽器上運行的節點直連推流的IPFS節點才能被廣播到,js版本的IPFS未來應該可以做到這一點,因此在這裏我用websocket來直連2個節點。
https://discuss.ipfs.io/t/pubsub-between-go-ipfs-and-js-ipfs/1744
既然這樣,我就只能通過websocket來妥協一下這個問題。
次時代直播網絡架構圖:
在這樣的網絡環境下,想要直播,每個家庭?或者每個內網,都應該至少運行一個IPFS節點,和至少一個RTMP推流拉流的軟件。當然我更希望的是集成在推流軟件裏,推流出來之後就直接是IPFS網絡了。
3.建立傳統直播服務器
安裝軟件的方式有很多,最簡單的不過apt install 安裝,但是在安裝Nginx軟件時發現apt 安裝的版本沒有rtmp模塊,需要源碼編譯安裝,自行加入nginx-rtmp-module模塊。
#!/bin/bash
wget http://nginx.org/download/nginx-1.15.9.tar.gz
wget https://github.com/arut/nginx-rtmp-module/archive/v1.2.1.tar.gz
tar -zxvf nginx-1.15.9.tar.gz
tar -zxvf v1.2.1.tar.gz
apt install gcc
cd nginx-1.15.9/
./configure --add-module=../nginx-rtmp-module-1.2.1
make
make install
在編譯時可能會因爲缺少依賴而失敗,補全所有必要的軟件包即可。
默認的安裝目錄在cd /usr/local/nginx/
,進入目錄啓動Nginx ./sbin/nginx
修改配置,進入rtmp模塊vim conf/nginx.conf
加入以下內容
rtmp {
server {
listen 1935;
application live {
live on;
}
}
}
我們重新加載Nginx./sbin/nginx -s reload
這樣傳統的直播服務器就搭建好了,我們來測試一下、
OBS設置:設置->流->自定義->服務器rtmp://172.16.10.170/live,流密鑰demo
OBS裏添加點東西,開始推流
打開一個播放器potplayer,打開->打開連接->rtmp://172.16.10.170/live/demo,然後就可以通過potplayer來觀看直播了
4.利用FFmpeg軟件拉取直播流,切片轉換成ts和m3u8直播切片格式
建立工作文件夾mkdir -p /root/demo /root/demo/ts
進入文件夾cd /root/demo/ts
啓動ffmpeg進程 ffmpeg -nostats -re -i rtmp://172.16.10.170/live/demo -f mpegts -c copy -hls_time 10 -hls_list_size 12 -f hls live.m3u8 > ../ffmpeg.log 2>&1
其中hls_time 是每個切片的時間,hls_list_size 是一個m3u8文件包含多少個切片文件。
然後我們看一下生成的文件長什麼樣子:
5.監控日誌,上傳文件到IPFS,推送消息
# -*- coding: utf-8 -*-
#!/usr/bin/env python
import os
import datetime
import pyinotify
import logging
import re
import ipfsapi
base_dir=r'/root/demo'
ipfscache={}
api = ipfsapi.connect('127.0.0.1', 5001)
def printlog():
try:
fd = open(os.path.join(base_dir, r'ts/live.m3u8'))
line = fd.read()
if line.strip():
pushtoipfs(line.strip())
fd.close()
except Exception, e:
print str(e)
class MyEventHandler(pyinotify.ProcessEvent):
def process_IN_MODIFY(self, event):
try:
printlog()
except Exception, e:
print str(e)
def pushtoipfs(m3u8):
pattern = re.compile(r'#EXTINF.*\n(.*)')
result1 = pattern.findall(m3u8)
newts = 0
for item in result1:
if not ipfscache.has_key(item):
print('add to ipfs: %s'%item)
res = api.add(os.path.join(base_dir, 'ts',item))
ipfscache[item]='/ipfs/%s'%res['Hash']
newts +=1
m3u8 = m3u8.replace(item, ipfscache[item])
# lastTS = ipfscache[result1[-1]]
if newts > 0:
res = api.add_str(m3u8)
lastm3u8 = '/ipfs/%s'%res
# api.pubsub_pub('live', u'%s\n'%lastTS)
api.pubsub_pub('livem3u8', u'%s\n'%lastm3u8)
def main():
printlog()
wm = pyinotify.WatchManager()
wm.add_watch(os.path.join(base_dir, r'ffmpeg.log'), pyinotify.ALL_EVENTS, rec=True)
eh = MyEventHandler()
# notifier
notifier = pyinotify.Notifier(wm, eh)
notifier.loop()
if __name__ == '__main__':
main()
6.網頁客戶端啓動IPFS節點,加載網頁播放器
<!DOCTYPE html>
<html lang="">
<head>
<title>LIVE ipfs</title>
<meta charset="utf-8">
<link href="video-js.min.css" rel="stylesheet">
<style type="text/css">body{margin:0;padding:0;}#live{display: none}</style>
</head>
<body>
<video id="live" class="video-js" controls preload="none" width="640" height="360" autoplay style="width: 100%; height: 100vh;">
</video>
<script src="jquery-3.3.1.min.js"></script>
<script src="ipfs.min.js"></script>
<script src="video.min.js"></script>
<script src="videojs-http-streaming.js"></script>
<script>
let url;
let ws;
let ipns;
let isload = 0;
let isplayer = 0;
videojs.Hls.xhr.beforeRequest = (options) => {
if(options.responseType !== 'arraybuffer'){
options.uri = url;
}
node.pubsub.peers('live_m3u8_'+ipns, (err, peerIds) => {
if(peerIds.length<1){
for (let i = ws.length - 1; i >= 0; i--) node.swarm.connect(ws[i]);
}
});
return options;
};
const node = new window.Ipfs({
repo: '/ipfs-' + Math.random(),
config: {
Addresses: {
Swarm: ['/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star/']
}
},
EXPERIMENTAL: {pubsub: true}
});
node.on('ready', () => {
isload = 1;
for (let i = ws.length - 1; i >= 0; i--) node.swarm.connect(ws[i]);
node.pubsub.subscribe('live_m3u8_'+ipns,(msg) => {
url = msg.data.toString();
if(isplayer===0){
isplayer = 1;
const video = document.getElementById('live');
const source = document.createElement("source");
source.src = url;
source.type = 'application/x-mpegURL';
video.appendChild(source);
$('#live').css({display:'block'});
const player = videojs('live');
setTimeout(()=>{
player.play();
},500);
}
});
});
$(() =>{
$.get('config.json',(config)=>{
ws = config.ws;
ipns = config.ipns;
});
setTimeout(()=>{
if(isload===0){
// window.location.reload()
}
},15000)
})
</script>
</body>
</html>
其中config.json 文件內容:
{
"version":0.1,
"ipns":"QmerPfuRp964AoNGMWBts9TQBMoPTYQpeq2rSAt3aywS6m",
"ws":[
"/ip4/172.16.10.170/tcp/9999/ws/ipfs/QmerPfuRp964AoNGMWBts9TQBMoPTYQpeq2rSAt3aywS6m"
]
}
其中這裏面的ipns表示我key的地址,將整個文件夾編輯好,推送至ipfs,就可以通過任意公共網關加載視頻了。
7.最終效果預覽
嘗試推送一部電影
目前有比較高的延遲我自己測試有緩衝40秒左右
8.其餘工作
- 優化流程,感覺有推流,有拉流,中間有些過程沒有必要。
- 對於服務端操作,寫成界面化,方便配置管理。
- 外圍程序編寫,直播間標題,直播間描述,彈幕評論系統。
- 對歷史分片進行記錄,可以重新發布上傳觀看錄播。
- 腳本檢測機制,推流時自動開啓,結束時自動關閉