如何解决Flask应用中m3u8流媒体的CORS跨域问题?
解决Flask代理m3u8/TS文件的CORS问题
问题场景
我正在构建一个Flask应用,使用m3u8和requests库提供m3u8流媒体视频服务,但从外部URL获取m3u8文件和TS片段时遭遇CORS错误,浏览器始终阻止请求。已尝试使用flask-cors库、手动添加CORS头、给代理路由添加@cross_origin()装饰器,但问题依然存在。
当前代码
Flask后端代码
from flask import Flask, request, jsonify, render_template import m3u8 import requests from flask_cors import CORS, cross_origin app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/extract', methods=['POST']) def extract_quality(): m3u8_url = request.json.get('url') if not m3u8_url: return jsonify({"error": "No URL provided"}), 400 try: playlist = m3u8.load(m3u8_url) qualities = [] for playlist_variant in playlist.playlists: resolution = playlist_variant.stream_info.resolution bandwidth = playlist_variant.stream_info.bandwidth uri = playlist_variant.uri qualities.append({ "resolution": resolution, "bandwidth": bandwidth, "uri": uri }) return jsonify(qualities) except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 CORS(app, resources={r"/*": {"origins": "*"}}) @app.route('/proxy', methods=['GET']) @cross_origin() def proxy(): url = request.args.get('url') if not url: return jsonify({"error": "No URL provided"}), 400 try: response = requests.get(url) if response.status_code == 200: return response.content, response.status_code, { 'Access-Control-Allow-Origin': '*', 'Content-Type': response.headers.get('Content-Type', 'application/octet-stream') } else: return jsonify({"error": f"Failed to fetch URL, status code: {response.status_code}"}), 500 except requests.exceptions.RequestException as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 @app.route('/tsproxy', methods=['GET']) @cross_origin() def ts_proxy(): ts_url = request.args.get('url') if not ts_url: return jsonify({"error": "No URL provided"}), 400 try: response = requests.get(ts_url) if response.status_code == 200: return response.content, response.status_code, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'video/MP2T' } else: return jsonify({"error": f"Failed to fetch TS file, status code: {response.status_code}"}), 500 except requests.exceptions.RequestException as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5500)
前端JavaScript代码
// JavaScript to handle fetching and playing the video function getQualities() { const url = document.getElementById("m3u8_url").value; if (!url) { alert("Please enter a URL!"); return; } fetch("/extract", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: url }), }) .then((response) => response.json()) .then((data) => { if (data.error) { alert(data.error); } else { const selector = document.getElementById("quality_selector"); selector.innerHTML = ""; // Clear previous results data.forEach((item) => { const option = document.createElement("option"); option.value = item.uri; option.textContent = `${item.resolution ? item.resolution.join("x") : "N/A"} - ${item.bandwidth} bps`; selector.appendChild(option); }); document.getElementById("quality_options").classList.remove("hidden"); } }) .catch((error) => { console.error("Error:", error); alert("An error occurred while fetching the data"); }); } function playVideo() { const video = document.getElementById("video"); const url = document.getElementById("quality_selector").value; if (!url) { alert("Please select a quality!"); return; } const proxyUrl = `http://192.168.1.15:5500/proxy?url=${encodeURIComponent(url)}`; if (Hls.isSupported()) { const hls = new Hls({ loader: function (config) { const originalLoader = new Hls.DefaultConfig.loader(config); const originalLoad = originalLoader.load.bind(originalLoader); // Update the TS segment URL to use the '/tsproxy' route originalLoader.load = function (context, config, callbacks) { if (context.type === "fragment") { context.url = `http://192.168.1.15:5500/tsproxy?url=${encodeURIComponent(context.url)}`; } return originalLoad(context, config, callbacks); }; return originalLoader; }, }); hls.loadSource(proxyUrl); hls.attachMedia(video); new Plyr(video); } else if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = proxyUrl; new Plyr(video); } else { alert("Your browser does not support HLS!"); } }
解决方案
1. 调整CORS初始化顺序
CORS配置必须在所有路由定义之前初始化,否则部分路由不会应用CORS规则。修改后端代码,把CORS(app, resources={r"/*": {"origins": "*"}})移到app = Flask(__name__)之后、第一个路由之前。
2. 合并代理路由,统一处理资源
无需分开/proxy和/tsproxy,用同一个路由处理所有资源请求,自动识别内容类型,减少重复代码:
@app.route('/proxy', methods=['GET']) def proxy(): url = request.args.get('url') if not url: return jsonify({"error": "No URL provided"}), 400 try: response = requests.get(url, stream=True) if response.status_code == 200: # 自动获取原响应的Content-Type content_type = response.headers.get('Content-Type', 'application/octet-stream') # 流式返回,优化大文件传输 return response.content, response.status_code, { 'Access-Control-Allow-Origin': '*', 'Content-Type': content_type, 'Cache-Control': 'public, max-age=3600' # 可选,添加缓存头优化性能 } else: return jsonify({"error": f"Failed to fetch resource, status code: {response.status_code}"}), response.status_code except requests.exceptions.RequestException as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500
3. 修复m3u8中的相对路径问题
当代理m3u8文件时,里面的TS片段URL如果是相对路径,需要转换成绝对路径后再返回给前端,否则前端会把相对路径指向自己的服务器。修改/extract路由:
@app.route('/extract', methods=['POST']) def extract_quality(): m3u8_url = request.json.get('url') if not m3u8_url: return jsonify({"error": "No URL provided"}), 400 try: playlist = m3u8.load(m3u8_url) # 获取原m3u8的基础URL,用于转换相对路径 base_url = m3u8_url.rsplit('/', 1)[0] + '/' qualities = [] for playlist_variant in playlist.playlists: resolution = playlist_variant.stream_info.resolution bandwidth = playlist_variant.stream_info.bandwidth # 转换相对URI为绝对URL uri = playlist_variant.uri if not uri.startswith('http'): uri = base_url + uri qualities.append({ "resolution": resolution, "bandwidth": bandwidth, "uri": uri }) return jsonify(qualities) except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500
4. 优化前端代理逻辑
前端只需要用同一个/proxy路由处理所有资源,无需区分m3u8和TS,简化Hls.js的loader配置:
function playVideo() { const video = document.getElementById("video"); const selectedUrl = document.getElementById("quality_selector").value; if (!selectedUrl) { alert("Please select a quality!"); return; } const proxyUrl = `/proxy?url=${encodeURIComponent(selectedUrl)}`; if (Hls.isSupported()) { const hls = new Hls({ loader: function (config) { const originalLoader = new Hls.DefaultConfig.loader(config); const originalLoad = originalLoader.load.bind(originalLoader); originalLoader.load = function (context, config, callbacks) { if (context.url) { // 所有资源都走同一个代理路由 context.url = `/proxy?url=${encodeURIComponent(context.url)}`; } return originalLoad(context, config, callbacks); }; return originalLoader; }, }); hls.loadSource(proxyUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play(); }); new Plyr(video); } else if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = proxyUrl; video.onloadedmetadata = () => { video.play(); }; new Plyr(video); } else { alert("Your browser does not support HLS!"); } }
5. 移除冗余的CORS装饰器
因为已经全局配置了CORS,所以可以去掉路由上的@cross_origin()装饰器,避免配置冲突。
修改后的完整后端代码
from flask import Flask, request, jsonify, render_template import m3u8 import requests from flask_cors import CORS app = Flask(__name__) # 全局CORS配置,放在所有路由之前 CORS(app, resources={r"/*": {"origins": "*"}}) @app.route('/') def index(): return render_template('index.html') @app.route('/extract', methods=['POST']) def extract_quality(): m3u8_url = request.json.get('url') if not m3u8_url: return jsonify({"error": "No URL provided"}), 400 try: playlist = m3u8.load(m3u8_url) base_url = m3u8_url.rsplit('/', 1)[0] + '/' qualities = [] for playlist_variant in playlist.playlists: resolution = playlist_variant.stream_info.resolution bandwidth = playlist_variant.stream_info.bandwidth uri = playlist_variant.uri if not uri.startswith('http'): uri = base_url + uri qualities.append({ "resolution": resolution, "bandwidth": bandwidth, "uri": uri }) return jsonify(qualities) except Exception as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 @app.route('/proxy', methods=['GET']) def proxy(): url = request.args.get('url') if not url: return jsonify({"error": "No URL provided"}), 400 try: response = requests.get(url, stream=True) if response.status_code == 200: content_type = response.headers.get('Content-Type', 'application/octet-stream') return response.content, response.status_code, { 'Access-Control-Allow-Origin': '*', 'Content-Type': content_type, 'Cache-Control': 'public, max-age=3600' } else: return jsonify({"error": f"Failed to fetch resource, status code: {response.status_code}"}), response.status_code except requests.exceptions.RequestException as e: return jsonify({"error": f"An error occurred: {str(e)}"}), 500 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5500)
内容的提问来源于stack exchange,提问作者Yousseif Muhammed
相关产品推荐
相关产品推荐

