通过HTTP/TCP控制VLC播放器

需求:编写一个python脚本,使用第三方中控或APP控制电脑媒体软件

准备:

  1. VLC播放器(下载地址:官方下载:VLC media player,最棒的开源播放器 - VideoLAN
  2. 安装python环境(下载地址:Download Python | Python.org) 和 IDE(spyder 或 pycharm)
  3. HTTP调试工具(postman)

具体步骤:

  1. 安装VLC播放器

  2. 配置VLC开启telnet端口(本地地址:127.0.0.1,端口号4212)

  3. 启动电脑telnet端口,需重启电脑

  4. 通过电脑-高级系统设置--系统属性--环境变量--PATH--编辑、新建添加VLC路径(需要重启命令行工具)

  5. 防火墙——高级设置——入站规则——新建-编辑——添加4212端口

  6. 测试监测指令:

    1. telnet 127.0.0.1 4212 登录VLC,登录后可以通过play pause stop 进行控制

    2. netstat -an | findstr 4212  端口监测

  7. 配置HTTP端口,VLC的HTTP端口默认8080,若要修改需要通过配置文件,文件目录:C:\Users\25207\AppData\Roaming\vlc(根据自己的目录修改)

  8. 端口监测指令:netstat -an | findstr 8080

  9. 通过spyder 运行脚本,测试验证telnet指令

  10. 通过postman测试验证http指令

测试使用py脚本:

# -*- coding: utf-8 -*-
"""
VLC Telnet/HTTP控制器 - 修复字节字符串问题
"""

import socket
import os
import time
import logging
import urllib.parse
import sys
import threading
import re
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("VLCTelnetControl")

# 默认配置
DEFAULT_PLAYER_IP = "127.0.0.1"
DEFAULT_PLAYER_PORT = 4212
DEFAULT_VLC_PASSWORD = "admin"
DEFAULT_HTTP_IP = "0.0.0.0"
DEFAULT_HTTP_PORT = 8080
DEFAULT_MEDIA_DIR = r"C:\Users\25207\Desktop"  # 默认媒体文件目录

# 全局变量
PLAYER_IP = DEFAULT_PLAYER_IP
PLAYER_PORT = DEFAULT_PLAYER_PORT
VLC_PASSWORD = DEFAULT_VLC_PASSWORD
HTTP_IP = DEFAULT_HTTP_IP
HTTP_PORT = DEFAULT_HTTP_PORT
MEDIA_DIR = DEFAULT_MEDIA_DIR  # 媒体文件目录
MEDIA_FILES = {}  # 存储媒体文件路径 {文件名: 完整路径}
CURRENT_MEDIA = None
PLAYER_STATE = "stopped"  # 状态: stopped, playing, paused

class VLCController:
    @staticmethod
    def send_command(command, media_path=None):
        """发送控制指令并处理响应"""
        global CURRENT_MEDIA, PLAYER_STATE
        
        s = VLCController.create_socket()
        if not s:
            return False
        
        try:
            # 命令映射
            command_map = {
                "pause": "pause",
                "stop": "stop",
                "replay": "seek 0",
                "volup": "volume +50",
                "voldown": "volume -50"
            }
            
            # 处理播放命令
            if command.startswith("play"):
                # 解析文件名
                parts = command.split(maxsplit=1)
                filename = parts[1] if len(parts) > 1 else None
                
                # 如果没有指定文件,但提供了media_path参数
                if not filename and media_path:
                    filename = os.path.basename(media_path)
                
                # 如果没有指定文件,使用当前媒体
                if not filename and CURRENT_MEDIA:
                    filename = os.path.basename(CURRENT_MEDIA)
                
                # 如果还没有媒体文件,返回错误
                if not filename:
                    logger.error("播放命令需要指定媒体文件")
                    return False
                
                # 获取完整路径
                full_path = VLCController.get_media_path(filename)
                if not full_path:
                    logger.error(f"找不到媒体文件: {filename}")
                    return False
                
                # 获取绝对路径并转换为VLC兼容格式
                abs_path = os.path.abspath(full_path)
                
                # 检查文件是否存在
                if not os.path.exists(abs_path):
                    logger.error(f"文件不存在: {abs_path}")
                    return False
                
                # 使用URL编码确保特殊字符正确处理
                unix_style_path = abs_path.replace('\\', '/')
                encoded_path = urllib.parse.quote(unix_style_path)
                full_url = f"file:///{encoded_path}"
                
                # 根据当前状态决定命令
                if PLAYER_STATE == "stopped" or CURRENT_MEDIA != abs_path:
                    # 停止状态或切换媒体 - 使用play命令加载并播放
                    play_command = f'play "{full_url}"'
                    logger.info(f"发送命令: {play_command}")
                    s.sendall((play_command + "\n").encode('utf-8'))
                    PLAYER_STATE = "playing"
                    CURRENT_MEDIA = abs_path
                elif PLAYER_STATE == "paused":
                    # 暂停状态 - 使用pause命令恢复播放
                    logger.info(f"发送命令: pause")
                    s.sendall(b"pause\n")
                    PLAYER_STATE = "playing"
                else:
                    # 已经在播放 - 只需发送播放命令
                    logger.info(f"发送命令: play")
                    s.sendall(b"play\n")
            
            # 处理停止命令
            elif command == "stop":
                logger.info(f"发送命令: stop")
                s.sendall(b"stop\n")
                PLAYER_STATE = "stopped"
            
            # 处理暂停命令
            elif command == "pause":
                logger.info(f"发送命令: pause")
                s.sendall(b"pause\n")
                # 切换状态
                if PLAYER_STATE == "playing":
                    PLAYER_STATE = "paused"
                elif PLAYER_STATE == "paused":
                    PLAYER_STATE = "playing"
            
            # 处理其他命令
            elif command in command_map:
                vlc_command = command_map[command]
                logger.info(f"发送命令: {vlc_command}")
                s.sendall((vlc_command + "\n").encode('utf-8'))
                
                # 特殊命令处理
                if command == "replay" and PLAYER_STATE != "stopped":
                    PLAYER_STATE = "playing"
            
            # 接收响应
            response = b""
            while True:
                try:
                    chunk = s.recv(1024)
                    if not chunk:
                        break
                    response += chunk
                    if b"> " in response:
                        break
                except socket.timeout:
                    logger.warning("接收响应超时")
                    break
            
            try:
                decoded_response = response.decode('utf-8', errors='replace')
                logger.info(f"响应: {decoded_response}")
            except UnicodeDecodeError:
                decoded_response = response.decode('latin-1', errors='replace')
                logger.info(f"响应(latin-1): {decoded_response}")
            
            # 检查错误
            if b"error" in response.lower():
                logger.error(f"命令执行错误: {response}")
                return False
            
            return True
        
        except Exception as e:
            logger.error(f"发送指令失败: {e}")
            return False
        finally:
            s.close()
    
    @staticmethod
    def create_socket():
        """创建并认证Telnet连接"""
        global PLAYER_IP, PLAYER_PORT, VLC_PASSWORD
        
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(5)  # 设置超时时间
            s.connect((PLAYER_IP, PLAYER_PORT))
            
            # 接收初始响应
            response = b""
            while True:
                chunk = s.recv(1024)
                if not chunk:
                    break
                response += chunk
                if b"Password:" in response or b"> " in response:
                    break
            
            try:
                decoded_response = response.decode('utf-8', errors='replace')
                logger.info(f"初始响应: {decoded_response}")
            except UnicodeDecodeError:
                decoded_response = response.decode('latin-1', errors='replace')
                logger.info(f"初始响应(latin-1): {decoded_response}")
            
            # 检查是否需要密码认证
            if b"Password:" in response:
                # 发送密码认证
                s.sendall((VLC_PASSWORD + "\n").encode('utf-8'))
                
                # 接收认证响应
                auth_response = b""
                while True:
                    chunk = s.recv(1024)
                    if not chunk:
                        break
                    auth_response += chunk
                    if b"> " in auth_response:
                        break
                
                try:
                    decoded_auth = auth_response.decode('utf-8', errors='replace')
                    logger.info(f"认证响应: {decoded_auth}")
                except UnicodeDecodeError:
                    decoded_auth = auth_response.decode('latin-1', errors='replace')
                    logger.info(f"认证响应(latin-1): {decoded_auth}")
                
                # 检查认证是否成功
                if b"> " not in auth_response:
                    logger.error("认证失败! 请检查密码是否正确")
                    s.close()
                    return None
                else:
                    logger.info("认证成功!")
            return s
        except Exception as e:
            logger.error(f"无法连接到播放器: {e}")
            return None
    
    @staticmethod
    def get_media_path(filename):
        """获取媒体文件的完整路径"""
        global MEDIA_DIR, MEDIA_FILES
        
        # 如果文件名是完整路径,直接返回
        if os.path.isabs(filename):
            return filename
        
        # 如果文件名在MEDIA_FILES字典中,返回对应路径
        if filename in MEDIA_FILES:
            return MEDIA_FILES[filename]
        
        # 尝试在媒体目录中查找
        full_path = os.path.join(MEDIA_DIR, filename)
        if os.path.exists(full_path):
            return full_path
        
        # 尝试在媒体目录中模糊匹配
        for file in os.listdir(MEDIA_DIR):
            if file.lower().startswith(filename.lower()):
                return os.path.join(MEDIA_DIR, file)
        
        # 找不到文件
        return None

def scan_media_files():
    """扫描媒体目录中的文件"""
    global MEDIA_DIR, MEDIA_FILES
    
    MEDIA_FILES = {}
    if not os.path.exists(MEDIA_DIR):
        return
    
    for file in os.listdir(MEDIA_DIR):
        if file.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.mp3', '.wav')):
            MEDIA_FILES[file] = os.path.join(MEDIA_DIR, file)
    
    logger.info(f"扫描到 {len(MEDIA_FILES)} 个媒体文件")

class VLCRequestHandler(BaseHTTPRequestHandler):
    """HTTP请求处理程序"""
    
    def do_GET(self):
        """处理GET请求"""
        # 解析请求路径和查询参数
        parsed = urlparse(self.path)
        path = parsed.path.lower()
        query = parse_qs(parsed.query)
        
        # 映射路径到命令
        command_map = {
            "/play": "play",
            "/pause": "pause",
            "/stop": "stop",
            "/replay": "replay",
            "/volup": "volup",
            "/voldown": "voldown",
            "/list": "list"
        }
        
        # 处理媒体文件列表请求
        if path == "/list":
            self.send_response(200)
            self.send_header('Content-type', 'text/plain; charset=utf-8')
            self.end_headers()
            files = "\n".join(MEDIA_FILES.keys())
            self.wfile.write(f"可用媒体文件:\n{files}".encode('utf-8'))
            return
        
        # 处理播放命令
        if path == "/play":
            # 获取文件名参数
            file_param = query.get('file', [''])[0]
            
            # 如果没有指定文件,使用当前媒体
            if not file_param and CURRENT_MEDIA:
                file_param = os.path.basename(CURRENT_MEDIA)
            
            # 如果还没有媒体文件,返回错误
            if not file_param:
                self.send_response(400)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write("请指定媒体文件,例如: /play?file=video.mp4".encode('utf-8'))
                return
            
            # 获取完整路径
            full_path = VLCController.get_media_path(file_param)
            if not full_path:
                self.send_response(404)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write(f"找不到媒体文件: {file_param}".encode('utf-8'))
                return
            
            # 执行播放命令
            success = VLCController.send_command(f"play {file_param}")
            
            if success:
                self.send_response(200)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write(f"正在播放: {file_param}".encode('utf-8'))
            else:
                self.send_response(500)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write(f"播放失败: {file_param}".encode('utf-8'))
            return
        
        # 处理其他命令
        if path in command_map:
            command = command_map[path]
            success = VLCController.send_command(command)
            
            if success:
                self.send_response(200)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write(f"命令 '{command}' 执行成功".encode('utf-8'))
            else:
                self.send_response(500)
                self.send_header('Content-type', 'text/plain; charset=utf-8')
                self.end_headers()
                self.wfile.write(f"命令 '{command}' 执行失败".encode('utf-8'))
        else:
            self.send_response(404)
            self.send_header('Content-type', 'text/plain; charset=utf-8')
            self.end_headers()
            self.wfile.write("无效命令. 可用命令: /play, /pause, /stop, /replay, /volup, /voldown, /list".encode('utf-8'))
    
    def log_message(self, format, *args):
        """重写日志方法"""
        logger.info("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), format % args))

def start_http_server():
    """启动HTTP服务器"""
    global HTTP_IP, HTTP_PORT
    
    server_address = (HTTP_IP, HTTP_PORT)
    httpd = HTTPServer(server_address, VLCRequestHandler)
    logger.info(f"HTTP服务器启动,监听地址: http://{HTTP_IP}:{HTTP_PORT}")
    logger.info("可用命令:")
    logger.info("  /play?file=视频文件   - 播放指定视频")
    logger.info("  /pause               - 暂停")
    logger.info("  /stop                - 停止")
    logger.info("  /replay              - 重新播放")
    logger.info("  /volup               - 音量增加")
    logger.info("  /voldown             - 音量减少")
    logger.info("  /list                - 列出可用媒体文件")
    
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        logger.error(f"HTTP服务器错误: {e}")
    finally:
        httpd.server_close()
        logger.info("HTTP服务器已停止")

def main():
    global PLAYER_IP, PLAYER_PORT, VLC_PASSWORD, HTTP_IP, HTTP_PORT, MEDIA_DIR
    
    # 获取用户输入
    print("=" * 50)
    print("VLC控制器配置")
    print("=" * 50)
    
    # 获取VLC播放器IP
    player_ip = input(f"请输入VLC播放器IP地址 [默认: {PLAYER_IP}]: ").strip()
    if player_ip:
        PLAYER_IP = player_ip
    
    # 获取VLC播放器密码
    vlc_password = input(f"请输入VLC Telnet密码 [默认: {VLC_PASSWORD}]: ").strip()
    if vlc_password:
        VLC_PASSWORD = vlc_password
    
    # 获取媒体目录
    media_dir = input(f"请输入媒体文件目录 [默认: {MEDIA_DIR}]: ").strip()
    if media_dir:
        MEDIA_DIR = media_dir
    
    # 检查媒体目录是否存在
    if not os.path.exists(MEDIA_DIR):
        logger.error(f"错误:媒体目录 '{MEDIA_DIR}' 不存在。")
        logger.error("请检查目录路径是否正确")
        sys.exit(1)
    
    # 扫描媒体文件
    scan_media_files()
    if not MEDIA_FILES:
        logger.warning(f"在目录 '{MEDIA_DIR}' 中没有找到媒体文件")
    else:
        logger.info(f"找到 {len(MEDIA_FILES)} 个媒体文件")
        for i, file in enumerate(list(MEDIA_FILES.keys())[:5], 1):
            logger.info(f"  {i}. {file}")
        if len(MEDIA_FILES) > 5:
            logger.info(f"  ... 共 {len(MEDIA_FILES)} 个文件")
    
    # 获取HTTP服务器配置
    print("\nHTTP服务器配置 (用于Loxone控制)")
    http_ip = input(f"请输入HTTP服务器监听IP [默认: {HTTP_IP}]: ").strip()
    if http_ip:
        HTTP_IP = http_ip
    
    http_port = input(f"请输入HTTP服务器端口 [默认: {HTTP_PORT}]: ").strip()
    if http_port:
        try:
            HTTP_PORT = int(http_port)
        except ValueError:
            logger.error("端口必须是数字,使用默认端口8080")
            HTTP_PORT = DEFAULT_HTTP_PORT
    
    # 启动HTTP服务器线程
    http_thread = threading.Thread(target=start_http_server, daemon=True)
    http_thread.start()
    
    # 主控制循环
    while True:
        print("\n可用命令:")
        print("  play [文件名] - 播放指定媒体文件")
        print("  pause         - 暂停播放")
        print("  stop          - 停止播放")
        print("  replay        - 重新播放")
        print("  volup         - 音量增加")
        print("  voldown       - 音量减少")
        print("  list          - 列出可用媒体文件")
        print(f"Loxone控制: http://<本机IP>:{HTTP_PORT}/play?file=文件名")
        print("输入 'exit' 退出程序")
        
        user_input = input("请输入命令: ").strip().lower()
        
        if not user_input:
            continue
            
        if user_input == 'exit':
            logger.info("正在退出程序...")
            break
        
        # 处理列表命令
        if user_input == 'list':
            print("\n可用媒体文件:")
            for file in MEDIA_FILES.keys():
                print(f"  - {file}")
            print(f"共 {len(MEDIA_FILES)} 个文件")
            continue
        
        # 处理播放命令(可能带文件名)
        if user_input.startswith("play"):
            VLCController.send_command(user_input)
            logger.info(f"当前状态: {PLAYER_STATE}")
        elif user_input in ['pause', 'stop', 'replay', 'volup', 'voldown']:
            VLCController.send_command(user_input)
            logger.info(f"当前状态: {PLAYER_STATE}")
        else:
            print("未知命令,请重新输入。")

if __name__ == "__main__":
    main()

后续迭代版本新增功能:V1.3      

VLC Telnet/HTTP控制器 - 
1、增加上一个、下一个、播放列表
2、增加运行脚本自动添加媒体文件到播放列表
3、增加播放自动全屏
4、增加开机自启动脚本,并打开VLC播放器
5、修复指定播放视频文件,同时支持使用play_index指定播放列表索引文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值