問題描述
使用 videojs 播放 cctv 直播源(http://cctvcnch5c.v.wscdns.com/live/cctv13_2/index.m3u8)時,出現跨域問題,如下所示:
Access to XMLHttpRequest at 'http://cctvcnch5c.v.wscdns.com/live/cctv13_2/index.m3u8' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
解決思路
跨域限制在 cctvcnch5c.v.wscdns.com
上,無法通過配置服務器解決限制。只能通過中間層接口請求或代理進行繞過(服務器訪問不受跨域協議限制)。
解決過程
中間層使用 express 框架,代理插件可使用 express-http-proxy
。
m3u8 直播源解析過程爲如下:
- 請求 m3u8 播放源地址
- 服務端返回 m3u8 純文本索引文件,其中包含各個媒體段的 url
- 客戶端解析 m3u8 的播放列表,再按序請求每一段的url,獲取ts數據流
其中,服務端返回的媒體段地址爲相對地址,默認前綴路徑爲步驟1的 m3u8 的請求路徑,所以還需要對步驟1返回的數據做處理,即步驟1需要使用接口模式,而步驟3使用代理模式(express-http-proxy
)。
具體代碼如下:
// main.js
var express = require('express');
var proxy = require('express-http-proxy');
var router = express.Router();
var urlParse = require('url').parse;
var controller = require('./controller.js');
// 代理直播源
router.get('/videos', controller.videoProxy);
router.use(
'/tsProxy',
proxy(
function(req) {
var target = urlParse(decodeURIComponent(req.query.url))
return target.host
},
{
parseReqBody: false, // 去除默認的 body,解決某些播放源 411 問題
proxyReqPathResolver: function(req) {
var target = urlParse(decodeURIComponent(req.query.url))
return target.path
},
}
)
)
express-http-proxy
默認自動解析並設置 req.body,這將導致 cctv 服務器識別到沒有 content-length 頭部的 request body,並返回 411 錯誤碼。所以需要在中間件配置上將 parseReqBody 設置爲false。
// controller.js
var axios = require('axios')
var controller = {};
controller.videoProxy = function(req, res) {
try {
var url = decodeURIComponent(req.query.url)
var parseUrl = urlParse(url)
var domain = parseUrl.protocol + '//' + parseUrl.host // http 域名地址
var m3u8Path = url.match(/\S+\//)[0] // http 至最後一個 '/' 字符
axios
.get(url)
.then(resp => {
var headers = resp.headers
var content = resp.data
// 內容爲 ts 文件地址則使用 tsProxy 的 path
var path = /BANDWIDTH/i.test(content)
? '/school/videos?url='
: '/school/tsProxy?url='
// 頭部信息全部返回
for (var key in headers) {
res.append(key, headers[key])
}
// 對 data 中目標文件字符串做處理
content = content.replace(/(?:\n)([^# \n]+\.\S+)/g, function(_, match) {
// 絕對地址,不做任何修改
if (/^http/.test(match)) {
return '\n' + path + encodeURIComponent(match)
}
// 相對地址:有文件結構的相對地址直接加域名,否則加上帶 path 的域名
return (
'\n' +
path +
encodeURIComponent((/^\//g.test(match) ? domain : m3u8Path) + match)
)
})
res.send(content)
})
.catch(e => {
res.json({
code: (e.response && e.response.status) || 404,
message: e.message || '',
success: false
})
})
} catch (e) {
res.json({
code: 400,
message: 'URI must be not empty!',
success: false
})
}
}
module.exports = controller;