问题描述
使用 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;