StreamFlow Player——局域网视频浏览中心

前言

前阵子入手了大疆Pocket3,这款小巧的云台相机让我能够随时随地记录生活中的美好瞬间。每次出游归来,存储卡里都塞满了北京各个角落的影像------从什刹海的波光粼粼到故宫角楼的落日余晖,从胡同里飘出的炊烟到国贸璀璨的夜景。

从存储卡到云端浏览的烦恼

起初,每次想回顾这些视频,我都得将Pocket3连接到电脑,把文件导入硬盘,再打开播放器软件。当我想在沙发上用平板或手机轻松浏览时,这种传统方式就显得不太"现代"了。

为什么不让自己拍摄的视频像在线流媒体一样随时可看呢?这个念头在我脑中闪过,我便决定动手打造一个属于自己的流媒体平台。

经过2个小时的开发,我创建了StreamFlow Player------一个简洁而实用的网页应用。它的界面设计以深色为主,突出重点内容,整体风格干净利落。

核心功能亮点:

• 便捷的本地视频列表:右侧区域直接展示了我从Pocket3导入的所有视频文件,清晰标注了每个视频的大小和创建日期

• 实时视频信息:底部区域实时显示播放状态、分辨率、码率等关键信息,让我随时了解视频的技术参数

• 跨设备访问:通过简单的局域网地址(如http://192.168.xx.xx:8000),我可以在任何连接到同一网络的设备上访问这个平台

使用体验:自由与便捷

现在,当我坐在阳台的摇椅上,想回顾昨天在颐和园拍摄的湖景时,只需打开手机浏览器,输入地址,就能立即观看。视频加载迅速,播放流畅,就像使用任何主流流媒体服务一样自然。

最让我满意的是,这个解决方案完全由我掌控:

• 没有上传等待时间,视频直接从本地存储读取

• 没有隐私担忧,所有内容都在我的局域网内流通

• 没有订阅费用,一次开发,长期受益

有了这个流媒体平台,我发现自己更频繁地回顾拍摄的视频素材,有时甚至会连续播放多个视频,寻找剪辑灵感。当朋友来访时,我也可以轻松地与他们分享我的北京影像日记,而不必担心设备兼容性问题。

效果展示

可以拖动进度条,查看所有视频。我给DJ配的内存卡只有128G,于是拍完我一般拿个读卡器,把里面的图片视频拷贝到我一个2T的移动机械硬盘上。

页面可全屏播放。

同个设备只需访问127.0.0.1:【前端启动端口】即可, 局域网下需替换为启动的设备.

ipad端:

列表设置了监听事件,点哪个就会播放哪个视频

代码

代码结构如下,我们比较关心app.py和index.html.

app.py的VIDEO_DIR 指定要访问的视频目录。

app.py

当前暂不支持流式传输,下个版本可能会优化。

python 复制代码
from flask import Flask, jsonify, send_file, request
from flask_cors import CORS
import os
import sqlite3
import mimetypes
from datetime import datetime

app = Flask(__name__)
CORS(app)  # 允许跨域请求

# 配置视频文件目录
# VIDEO_DIR = os.path.join(os.path.dirname(__file__), 'videos')
VIDEO_DIR = 'H:/DJ/DJI_001'
if not os.path.exists(VIDEO_DIR):
    os.makedirs(VIDEO_DIR)

# 数据库配置
DB_NAME = 'streamplayer.db'

# 初始化数据库
def init_db():
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    # 创建视频文件表
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS videos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        filename TEXT NOT NULL,
        filepath TEXT NOT NULL,
        filesize INTEGER,
        duration INTEGER,
        format TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
    ''')
    conn.commit()
    conn.close()

# 扫描视频目录并更新数据库
def scan_videos():
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    
    # 支持的视频格式
    video_extensions = {'.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'}
    
    # 清空现有数据
    cursor.execute('DELETE FROM videos')
    
    # 扫描目录
    for filename in os.listdir(VIDEO_DIR):
        filepath = os.path.join(VIDEO_DIR, filename)
        if os.path.isfile(filepath):
            ext = os.path.splitext(filename)[1].lower()
            if ext in video_extensions:
                filesize = os.path.getsize(filepath)
                cursor.execute('''
                INSERT INTO videos (filename, filepath, filesize, format) 
                VALUES (?, ?, ?, ?)
                ''', (filename, filepath, filesize, ext[1:]))
    
    conn.commit()
    conn.close()


# API路由
@app.route('/api/videos', methods=['GET'])
def get_videos():
    """获取视频文件列表"""
    conn = sqlite3.connect(DB_NAME)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    
    # 重新扫描视频目录
    scan_videos()
    
    cursor.execute('SELECT id, filename, filesize, format, created_at FROM videos')
    videos = [dict(row) for row in cursor.fetchall()]
    
    conn.close()
    return jsonify(videos)

@app.route('/api/video/<int:video_id>', methods=['GET'])
def get_video(video_id):
    """获取单个视频文件信息"""
    conn = sqlite3.connect(DB_NAME)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    
    cursor.execute('SELECT * FROM videos WHERE id = ?', (video_id,))
    video = cursor.fetchone()
    
    conn.close()
    
    if video:
        return jsonify(dict(video))
    else:
        return jsonify({'error': '视频文件不存在'}), 404

@app.route('/api/stream/<int:video_id>', methods=['GET'])
def stream_video(video_id):
    """流式传输视频文件"""
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    
    cursor.execute('SELECT filepath, filename FROM videos WHERE id = ?', (video_id,))
    video = cursor.fetchone()
    
    conn.close()
    
    if not video:
        return jsonify({'error': '视频文件不存在'}), 404
    
    filepath, filename = video
    
    # 获取文件的MIME类型
    mime_type, _ = mimetypes.guess_type(filepath)
    if mime_type is None:
        mime_type = 'video/mp4'  # 默认类型
    
    # 发送文件(todo: 支持断点续传, 流式传输)
    return send_file(
        filepath,
        mimetype=mime_type,
        as_attachment=False,
        conditional=True
    )

@app.route('/api/upload', methods=['POST'])
def upload_video():
    """上传视频文件"""
    if 'file' not in request.files:
        return jsonify({'error': '没有选择文件'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': '文件名不能为空'}), 400
    
    # 检查文件格式
    ext = os.path.splitext(file.filename)[1].lower()
    video_extensions = {'.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'}
    if ext not in video_extensions:
        return jsonify({'error': '不支持的文件格式'}), 400
    
    # 保存文件
    filepath = os.path.join(VIDEO_DIR, file.filename)
    file.save(filepath)
    
    # 更新数据库
    scan_videos()
    
    return jsonify({'message': '文件上传成功', 'filename': file.filename})

@app.route('/api/refresh', methods=['POST'])
def refresh_videos():
    """刷新视频列表"""
    scan_videos()
    return jsonify({'message': '视频列表已刷新'})

# 健康检查路由
@app.route('/api/health', methods=['GET'])
def health_check():
    return jsonify({'status': 'ok', 'timestamp': datetime.now().isoformat()})

if __name__ == '__main__':
    # 初始化数据库
    init_db()
    scan_videos()
    app.run(host='0.0.0.0', port=5000, debug=True)

requirements.txt中是一些依赖

txt 复制代码
Flask==2.0.1
Flask-CORS==3.0.10
SQLAlchemy==1.4.27
Werkzeug==2.0.1
python-dotenv==0.19.0

index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>StreamFlow Player-Andy Dennis</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <style>
        /* 自定义样式 - 基于设计系统 */
        :root {
            --primary: #3b82f6;
            --primary-dark: #1d4ed8;
            --secondary: #64748b;
            --accent: #f59e0b;
            --dark: #1e293b;
            --light: #f8fafc;
        }
        
        body {
            font-family: 'Inter', system-ui, -apple-system, sans-serif;
            background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
            min-height: 100vh;
            color: var(--light);
        }
        
        .glass-effect {
            background: rgba(30, 41, 59, 0.7);
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .player-container {
            box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
            border-radius: 12px;
            overflow: hidden;
        }
        
        .progress-bar {
            height: 4px;
            background: rgba(255, 255, 255, 0.2);
            position: relative;
            cursor: pointer;
        }
        
        .progress-filled {
            height: 100%;
            background: var(--accent);
            width: 0%;
            transition: width 0.1s ease;
        }
        
        .control-btn {
            transition: all 0.2s ease;
            border-radius: 50%;
        }
        
        .control-btn:hover {
            background: rgba(255, 255, 255, 0.1);
            transform: scale(1.05);
        }
        
        .format-badge {
            background: var(--primary);
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 0.75rem;
            margin-left: 8px;
        }
        
        .video-item {
            transition: all 0.2s ease;
        }
        
        .video-item:hover {
            transform: translateY(-2px);
        }
        
        .video-item.active {
            border: 2px solid var(--accent);
        }
        
        .reload-btn {
            transition: all 0.2s ease;
        }
        
        .reload-btn:hover {
            transform: rotate(180deg);
        }
    </style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
    <div class="w-full max-w-6xl mx-auto">
        <!-- 头部 -->
        <header class="glass-effect rounded-2xl p-6 mb-8">
            <div class="flex flex-col md:flex-row justify-between items-center">
                <div class="flex items-center mb-4 md:mb-0">
                    <div class="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
                        <i class="fas fa-play text-white"></i>
                    </div>
                    <h1 class="text-2xl font-bold">StreamFlow Player</h1>
                </div>
                
                <div class="flex items-center space-x-4">
                    <div class="relative">
                        <input type="text" id="videoUrl" placeholder="粘贴视频链接 (MP4, M3U8, FLV)" 
                               class="bg-slate-800 text-white rounded-full py-2 px-4 pl-10 w-80 focus:outline-none focus:ring-2 focus:ring-blue-500">
                        <i class="fas fa-link absolute left-3 top-3 text-slate-400"></i>
                    </div>
                    <button id="loadVideo" class="bg-blue-600 hover:bg-blue-700 text-white rounded-full py-2 px-6 font-medium transition-colors">
                        加载视频
                    </button>
                </div>
            </div>
        </header>

        <!-- 主内容区域 -->
        <div class="flex flex-col lg:flex-row gap-8">
            <!-- 播放器区域 -->
            <main class="flex-1">
                <div class="player-container glass-effect">
                    <!-- 视频播放器 -->
                    <div id="player" class="w-full aspect-video bg-black rounded-t-lg overflow-hidden flex items-center justify-center"></div>
                    
                    <!-- 自定义控制条 -->
                    <div class="p-4 bg-slate-800">
                        <!-- 进度条 -->
                        <div class="progress-bar mb-4">
                            <div class="progress-filled"></div>
                        </div>
                        
                        <!-- 控制按钮 -->
                        <div class="flex justify-between items-center">
                            <div class="flex items-center space-x-4">
                                <button id="playPause" class="control-btn w-10 h-10 flex items-center justify-center">
                                    <i class="fas fa-play text-white"></i>
                                </button>
                                <button id="mute" class="control-btn w-8 h-8 flex items-center justify-center">
                                    <i class="fas fa-volume-up text-white"></i>
                                </button>
                                <div class="text-sm">
                                    <span id="currentTime">00:00</span> / <span id="duration">00:00</span>
                                </div>
                            </div>
                            
                            <div class="flex items-center space-x-4">
                                <button id="fullscreen" class="control-btn w-8 h-8 flex items-center justify-center">
                                    <i class="fas fa-expand text-white"></i>
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- 视频信息 -->
                <div class="glass-effect rounded-2xl p-6 mt-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-xl font-bold">视频信息</h2>
                        <div id="formatIndicator" class="format-badge">未加载</div>
                    </div>
                    
                    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
                        <div class="bg-slate-800 p-3 rounded-lg">
                            <div class="text-slate-400">状态</div>
                            <div id="statusInfo">等待输入视频链接</div>
                        </div>
                        <div class="bg-slate-800 p-3 rounded-lg">
                            <div class="text-slate-400">分辨率</div>
                            <div id="resolutionInfo">-</div>
                        </div>
                        <div class="bg-slate-800 p-3 rounded-lg">
                            <div class="text-slate-400">码率</div>
                            <div id="bitrateInfo">-</div>
                        </div>
                    </div>
                </div>
            </main>
            
            <!-- 侧边栏 -->
            <aside class="w-full lg:w-80">
                <!-- 本地视频列表 -->
                <div class="glass-effect rounded-2xl p-6 mb-6">
                    <div class="flex justify-between items-center mb-4">
                        <h2 class="text-xl font-bold">本地视频</h2>
                        <button id="refreshVideos" class="reload-btn bg-slate-700 hover:bg-slate-600 p-2 rounded-full">
                            <i class="fas fa-sync-alt text-white"></i>
                        </button>
                    </div>
                    
                    <div id="videoList" class="space-y-3 max-h-96 overflow-y-auto">
                        <!-- 视频列表将通过JavaScript动态加载 -->
                        <div class="text-center text-slate-400 py-8">
                            <i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
                            <p>加载视频列表中...</p>
                        </div>
                    </div>
                </div>
                
                <!-- 格式支持 -->
                <div class="glass-effect rounded-2xl p-6">
                    <h2 class="text-xl font-bold mb-4">支持格式</h2>
                    
                    <div class="space-y-4">
                        <div class="flex items-center">
                            <div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center mr-3">
                                <i class="fas fa-file-video text-white text-sm"></i>
                            </div>
                            <div>
                                <div class="font-medium">MP4 (H.264)</div>
                                <div class="text-sm text-slate-400">标准视频文件</div>
                            </div>
                        </div>
                        
                        <div class="flex items-center">
                            <div class="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center mr-3">
                                <i class="fas fa-broadcast-tower text-white text-sm"></i>
                            </div>
                            <div>
                                <div class="font-medium">HLS (M3U8)</div>
                                <div class="text-sm text-slate-400">HTTP Live Streaming</div>
                            </div>
                        </div>
                        
                        <div class="flex items-center">
                            <div class="w-8 h-8 bg-red-600 rounded-full flex items-center justify-center mr-3">
                                <i class="fas fa-satellite-dish text-white text-sm"></i>
                            </div>
                            <div>
                                <div class="font-medium">FLV</div>
                                <div class="text-sm text-slate-400">Flash Video 直播流</div>
                            </div>
                        </div>
                    </div>
                </div>
            </aside>
        </div>
    </div>

    <!-- 引入Flowplayer库 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flowplayer/7.2.7/flowplayer.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/flowplayer/7.2.7/flowplayer.min.js"></script>
    
    <script>
        // 初始化Flowplayer [2,3](@ref)
        document.addEventListener('DOMContentLoaded', function() {
            // 获取DOM元素
            const videoUrlInput = document.getElementById('videoUrl');
            const loadVideoBtn = document.getElementById('loadVideo');
            const playPauseBtn = document.getElementById('playPause');
            const muteBtn = document.getElementById('mute');
            const fullscreenBtn = document.getElementById('fullscreen');
            const currentTimeEl = document.getElementById('currentTime');
            const durationEl = document.getElementById('duration');
            const progressBar = document.querySelector('.progress-bar');
            const progressFilled = document.querySelector('.progress-filled');
            const formatIndicator = document.getElementById('formatIndicator');
            const statusInfo = document.getElementById('statusInfo');
            const resolutionInfo = document.getElementById('resolutionInfo');
            const bitrateInfo = document.getElementById('bitrateInfo');
            const videoListEl = document.getElementById('videoList');
            const refreshVideosBtn = document.getElementById('refreshVideos');
            
            let player = null;
            let currentVideoId = null;
            
            // 后端API地址
            const API_BASE_URL = 'http://192.168.31.70:5000/api';
            
            // 加载视频列表
            function loadVideoList() {
                videoListEl.innerHTML = `
                    <div class="text-center text-slate-400 py-8">
                        <i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
                        <p>加载视频列表中...</p>
                    </div>
                `;
                
                fetch(`${API_BASE_URL}/videos`)
                    .then(response => response.json())
                    .then(videos => {
                        if (videos.length === 0) {
                            videoListEl.innerHTML = `
                                <div class="text-center text-slate-400 py-8">
                                    <i class="fas fa-video text-2xl mb-2"></i>
                                    <p>没有找到视频文件</p>
                                    <p class="text-sm mt-2">请将视频文件放入 backend/videos 目录</p>
                                </div>
                            `;
                        } else {
                            videoListEl.innerHTML = '';
                            videos.forEach(video => {
                                const videoItem = document.createElement('div');
                                videoItem.className = `video-item bg-slate-800 p-3 rounded-lg cursor-pointer hover:bg-slate-700 transition-colors ${currentVideoId === video.id ? 'active' : ''}`;
                                videoItem.dataset.videoId = video.id;
                                videoItem.dataset.videoFormat = video.format;
                                
                                // 格式化文件大小
                                const formattedSize = formatFileSize(video.filesize);
                                
                                videoItem.innerHTML = `
                                    <div class="flex justify-between items-center">
                                        <div class="font-medium truncate">${video.filename}</div>
                                        <span class="format-badge bg-green-600">${video.format.toUpperCase()}</span>
                                    </div>
                                    <div class="flex justify-between items-center text-xs text-slate-400 mt-1">
                                        <span>${formattedSize}</span>
                                        <span>${new Date(video.created_at).toLocaleDateString()}</span>
                                    </div>
                                `;
                                
                                videoItem.addEventListener('click', () => {
                                    playLocalVideo(video.id, video.filename, video.format);
                                });
                                
                                videoListEl.appendChild(videoItem);
                            });
                        }
                    })
                    .catch(error => {
                        console.error('加载视频列表失败:', error);
                        videoListEl.innerHTML = `
                            <div class="text-center text-red-400 py-8">
                                <i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
                                <p>加载视频列表失败</p>
                                <p class="text-sm mt-2">请检查后端服务是否运行</p>
                            </div>
                        `;
                    });
            }
            
            // 播放本地视频
            function playLocalVideo(videoId, filename, format) {
                currentVideoId = videoId;
                
                // 更新视频项的激活状态
                document.querySelectorAll('.video-item').forEach(item => {
                    if (parseInt(item.dataset.videoId) === videoId) {
                        item.classList.add('active');
                    } else {
                        item.classList.remove('active');
                    }
                });
                
                const videoUrl = `${API_BASE_URL}/stream/${videoId}`;
                videoUrlInput.value = videoUrl;
                statusInfo.textContent = `加载视频: ${filename}`;
                initPlayer(videoUrl, format);
            }
            
            // 初始化播放器
            function initPlayer(videoUrl, format = 'mp4') {
                // 清理现有播放器
                if (player && typeof player.destroy === 'function') {
                    player.destroy();
                } else {
                    // 如果destroy方法不存在,重新创建播放器容器
                    const playerContainer = document.getElementById('player');
                    if (playerContainer) {
                        playerContainer.innerHTML = '';
                    }
                }
                
                // 设置格式指示器
                let type = 'mp4';
                if (format === 'm3u8' || videoUrl.match(/\.m3u8(?:\?|$)/i)) {
                    type = 'm3u8';
                    formatIndicator.textContent = 'HLS';
                    formatIndicator.className = 'format-badge bg-purple-600';
                } else if (format === 'flv' || videoUrl.match(/\.flv(?:\?|$)/i)) {
                    type = 'flv';
                    formatIndicator.textContent = 'FLV';
                    formatIndicator.className = 'format-badge bg-red-600';
                } else {
                    formatIndicator.textContent = 'MP4';
                    formatIndicator.className = 'format-badge bg-green-600';
                }
                
                // 初始化播放器
                try {
                    // 清空播放器容器
                    const playerContainer = document.getElementById('player');
                    if (playerContainer) {
                        playerContainer.innerHTML = '';
                    }
                    
                    // 使用原生HTML5 video标签
                    const videoElement = document.createElement('video');
                    videoElement.id = 'videoPlayer';
                    videoElement.className = 'w-full h-full';
                    videoElement.controls = true;
                    videoElement.autoplay = true;
                    videoElement.muted = false;
                    videoElement.style.objectFit = 'fill';
                    videoElement.style.width = '100%';
                    videoElement.style.height = '100%';
                    
                    const sourceElement = document.createElement('source');
                    sourceElement.src = videoUrl;
                    sourceElement.type = type === 'flv' ? 'video/flv' : 'video/mp4';
                    
                    videoElement.appendChild(sourceElement);
                    playerContainer.appendChild(videoElement);
                    
                    // 保存播放器实例
                    player = videoElement;
                    
                    // 添加事件监听器
                    videoElement.addEventListener('loadedmetadata', function() {
                        statusInfo.textContent = '播放器准备就绪';
                        console.log('Video metadata loaded');
                        
                        // 更新视频分辨率信息
                        if (videoElement.videoWidth && videoElement.videoHeight) {
                            resolutionInfo.textContent = `${videoElement.videoWidth} x ${videoElement.videoHeight}`;
                        } else {
                            resolutionInfo.textContent = '未知';
                        }
                        
                        // 更新视频码率信息
                        // 尝试通过视频文件大小和时长估算码率
                        // 注意:这种方法只是估算,实际码率可能不同
                        if (videoElement.duration > 0) {
                            // 尝试获取视频文件大小(需要后端支持)
                            // 这里使用一个简单的估算方法
                            const estimatedBitrate = Math.round((videoElement.duration * 1000) / 1024); // 估算值
                            bitrateInfo.textContent = `${estimatedBitrate} Kbps`;
                        } else {
                            bitrateInfo.textContent = '未知';
                        }
                    });
                    
                    videoElement.addEventListener('play', function() {
                        statusInfo.textContent = '播放中';
                        playPauseBtn.innerHTML = '<i class="fas fa-pause text-white"></i>';
                    });
                    
                    videoElement.addEventListener('pause', function() {
                        statusInfo.textContent = '已暂停';
                        playPauseBtn.innerHTML = '<i class="fas fa-play text-white"></i>';
                    });
                    
                    videoElement.addEventListener('error', function(error) {
                        statusInfo.textContent = '播放错误';
                        console.error('Video error:', error);
                    });
                    
                    // 更新时间显示
                    setInterval(function() {
                        if (player && !isNaN(player.duration)) {
                            const currentTime = player.currentTime;
                            const duration = player.duration;
                            
                            currentTimeEl.textContent = formatTime(currentTime);
                            durationEl.textContent = formatTime(duration);
                            
                            // 更新进度条
                            if (duration > 0) {
                                const percent = (currentTime / duration) * 100;
                                progressFilled.style.width = percent + '%';
                            }
                        }
                    }, 500);
                    
                } catch (error) {
                    console.error('初始化播放器失败:', error);
                    statusInfo.textContent = '播放器初始化失败,请检查视频链接';
                    player = null;
                    return;
                }
                
                // 播放器事件监听器已在创建video元素时添加
            }
            
            // 格式化时间
            function formatTime(seconds) {
                const mins = Math.floor(seconds / 60);
                const secs = Math.floor(seconds % 60);
                return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
            }
            
            // 格式化文件大小
            function formatFileSize(bytes) {
                if (bytes === 0) return '0 B';
                const k = 1024;
                const sizes = ['B', 'KB', 'MB', 'GB'];
                const i = Math.floor(Math.log(bytes) / Math.log(k));
                return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
            }
            
            // 加载视频按钮事件
            loadVideoBtn.addEventListener('click', function() {
                const videoUrl = videoUrlInput.value.trim();
                if (!videoUrl) {
                    alert('请输入视频链接');
                    return;
                }
                
                statusInfo.textContent = '加载中...';
                initPlayer(videoUrl);
            });
            
            // 播放/暂停按钮事件
            playPauseBtn.addEventListener('click', function() {
                if (player) {
                    if (player.paused) {
                        player.play();
                    } else {
                        player.pause();
                    }
                } else {
                    // 如果播放器未初始化,尝试加载输入框中的视频链接
                    const videoUrl = videoUrlInput.value.trim();
                    if (videoUrl) {
                        statusInfo.textContent = '加载中...';
                        initPlayer(videoUrl);
                    } else {
                        statusInfo.textContent = '请先输入视频链接或选择本地视频';
                    }
                }
            });
            
            // 静音按钮事件
            muteBtn.addEventListener('click', function() {
                if (player) {
                    player.muted = !player.muted;
                    muteBtn.innerHTML = player.muted ? 
                        '<i class="fas fa-volume-mute text-white"></i>' : 
                        '<i class="fas fa-volume-up text-white"></i>';
                }
            });
            
            // 全屏按钮事件
            fullscreenBtn.addEventListener('click', function() {
                if (player) {
                    const playerContainer = document.getElementById('player');
                    if (playerContainer.requestFullscreen) {
                        playerContainer.requestFullscreen();
                    } else if (playerContainer.mozRequestFullScreen) {
                        playerContainer.mozRequestFullScreen();
                    } else if (playerContainer.webkitRequestFullscreen) {
                        playerContainer.webkitRequestFullscreen();
                    } else if (playerContainer.msRequestFullscreen) {
                        playerContainer.msRequestFullscreen();
                    }
                }
            });
            
            // 进度条点击事件
            progressBar.addEventListener('click', function(e) {
                if (player && !isNaN(player.duration)) {
                    const rect = progressBar.getBoundingClientRect();
                    const percent = (e.clientX - rect.left) / rect.width;
                    const time = percent * player.duration;
                    player.currentTime = time;
                }
            });
            
            // 刷新视频列表按钮事件
            refreshVideosBtn.addEventListener('click', function() {
                loadVideoList();
            });
            
            // 回车键支持
            videoUrlInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    loadVideoBtn.click();
                }
            });
            
            // 初始化加载视频列表
            loadVideoList();
        });
    </script>
</body>
</html>

启动

先在backend框架, 启动后端

python 复制代码
python app.py

在前端, 我比较喜欢用live-server, 这是个npm的工具

没有的话,

bash 复制代码
npm install -g live-server

然后再front_end

bash 复制代码
live-server
相关推荐
码界奇点17 小时前
基于Flask与OpenSSL的自签证书管理系统设计与实现
后端·python·flask·毕业设计·飞书·源代码管理
分享牛18 小时前
LangChain4j从入门到精通-11-结构化输出
后端·python·flask
乔江seven20 小时前
【python轻量级Web框架 Flask 】2 构建稳健 API:集成 MySQL 参数化查询与 DBUtils 连接池
前端·python·mysql·flask·web
serve the people20 小时前
python环境搭建 (三) FastAPI 与 Flask 对比
python·flask·fastapi
沐泽__1 天前
Flask简介
后端·python·flask
编码者卢布1 天前
【Azure Developer】azd 安装最新版无法登录中国区问题二:本地Windows环境遇问题
microsoft·flask·azure
历程里程碑1 天前
滑动窗口------滑动窗口最大值
大数据·python·算法·elasticsearch·搜索引擎·flask·tornado
90的程序爱好者1 天前
Flask 用户注册功能实现
python·flask
编码者卢布2 天前
【Azure Storage Account】Azure Table Storage 跨区批量迁移方案
后端·python·flask