You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 22:33:10