MPlayer 媒体播放器系统代码详解
一、程序概览
1.1 程序功能
这是一个基于C语言的命令行媒体播放器控制系统,通过管道(FIFO)与MPlayer进程通信,实现对音频/视频文件的播放控制。
1.2 核心技术
-
进程间通信(IPC):使用命名管道(FIFO)
-
多进程编程:fork()创建子进程运行MPlayer
-
信号处理:处理程序退出信号
-
文件系统操作:目录扫描、文件过滤
二、代码结构详解
2.1 头文件包含
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数(exit, atoi等)
#include <unistd.h> // UNIX标准函数(fork, execlp等)
#include <string.h> // 字符串处理函数
#include <fcntl.h> // 文件控制(open等)
#include <sys/types.h> // 系统数据类型
#include <sys/stat.h> // 文件状态(mkfifo等)
#include <sys/wait.h> // 进程等待(waitpid)
#include <signal.h> // 信号处理
#include <time.h> // 时间函数
#include <dirent.h> // 目录操作
#include <errno.h> // 错误号定义
2.2 数据结构和全局变量
2.2.1 播放列表结构
typedef struct {
char name[10][512]; // 二维数组:最多10个文件,每个路径最长511字符
int total; // 实际找到的文件数量
int current; // 当前播放的索引(0-based)
} LIST;
设计说明:
-
使用静态数组简化内存管理
-
name[10][512]:限制最大文件数为10,路径长度不超过512字节 -
current用于循环播放时的索引控制
2.2.2 全局变量
#define MEDIA_PATH "/home/linux/Music" // 媒体文件搜索路径
LIST list = {0}; // 播放列表实例,初始化为0
pid_t mplayer_pid = 0; // MPlayer进程ID,0表示无进程运行
int fifo_fd = -1; // 管道文件描述符,-1表示未打开
三、核心函数详解
3.1 初始化函数 do_init()
void do_init()
{
// 1. 打开媒体目录
DIR *dir = opendir(MEDIA_PATH);
if (NULL == dir) {
perror("打开目录失败");
exit(1); // 致命错误,直接退出
}
// 2. 初始化列表
list.total = 0;
list.current = 0;
// 3. 遍历目录,筛选媒体文件
while (1) {
struct dirent *info = readdir(dir);
if (NULL == info) {
break; // 遍历结束
}
// 提取文件扩展名(最后3个字符)
char *ext = &info->d_name[strlen(info->d_name) - 3];
// 检查是否为目标格式
if (strcmp(ext, "mp4") == 0 ||
strcmp(ext, "mp3") == 0 ||
strcmp(ext, "flv") == 0) {
// 构建完整路径并保存
sprintf(list.name[list.total++], "%s/%s",
MEDIA_PATH, info->d_name);
}
}
// 4. 关闭目录
closedir(dir);
// 5. 创建命名管道(先删除旧的)
unlink("/tmp/fifo"); // 删除可能存在的旧管道
int ret = mkfifo("/tmp/fifo", 0666); // 创建新管道,权限rw-rw-rw-
if (ret == -1 && errno != EEXIST) { // 创建失败且不是已存在
perror("mkfifo");
exit(1);
}
}
关键点:
-
简单的扩展名检查:仅检查最后3个字符,假设文件都有正确扩展名
-
管道创建:使用
/tmp/fifo作为通信管道 -
错误处理:目录打开失败和管道创建失败是致命错误
3.2 命令发送函数 send_cmd()
void send_cmd(char *cmd)
{
// 1. 检查管道是否已打开
if (fifo_fd < 0) {
fifo_fd = open("/tmp/fifo", O_WRONLY); // 只写方式打开
if (fifo_fd < 0) {
perror("打开管道失败");
return; // 非致命错误,只输出信息
}
}
// 2. 格式化命令(添加换行符)
char buf[256] = {0};
sprintf(buf, "%s\n", cmd); // MPlayer要求命令以换行结束
// 3. 发送命令
int ret = write(fifo_fd, buf, strlen(buf));
// 4. 错误处理
if (ret < 0) {
perror("发送命令失败");
close(fifo_fd); // 关闭失败的连接
fifo_fd = -1; // 标记为需要重新打开
} else {
printf("发送命令: %s\n", cmd); // 成功反馈
}
}
通信协议:
-
命令格式:
命令字符串 + "\n" -
管道位置:
/tmp/fifo -
连接策略:保持连接,失败时重连
3.3 播放函数 do_play()
void do_play(char *file)
{
// 1. 停止当前运行的MPlayer(如果存在)
if (mplayer_pid > 0) {
send_cmd("quit"); // 优雅退出命令
usleep(100000); // 等待100ms让MPlayer退出
kill(mplayer_pid, SIGTERM); // 发送终止信号
waitpid(mplayer_pid, NULL, 0); // 等待进程结束
mplayer_pid = 0; // 清除进程ID记录
}
// 2. 关闭旧管道连接
if (fifo_fd >= 0) {
close(fifo_fd);
fifo_fd = -1;
}
// 3. 创建新进程播放
mplayer_pid = fork(); // 创建子进程
if (mplayer_pid == 0) { // 子进程
// 执行MPlayer命令
execlp("mplayer", // 使用PATH查找
"mplayer", // 程序名
"-slave", // 启用从模式(接受命令)
"-input", // 指定输入方式
"file=/tmp/fifo", // 从管道读取命令
file, // 要播放的文件
"-quiet", // 减少输出
NULL); // 参数结束
// 如果execlp返回,说明执行失败
perror("启动 mplayer 失败");
exit(1); // 子进程退出
}
// 父进程继续执行
printf("开始播放: %s\n", strrchr(file, '/') + 1); // 显示文件名
}
进程管理策略:
-
先停止再启动:避免多个MPlayer同时运行
-
双重退出机制:先发送quit命令,再发送SIGTERM信号
-
进程回收:使用waitpid避免僵尸进程
3.4 列表管理函数
do_list():显示并选择歌曲
int do_list()
{
// 1. 检查列表是否为空
if (list.total == 0) {
printf("播放列表为空!\n");
return -1;
}
// 2. 显示列表(只显示文件名,不显示完整路径)
printf("\n=== 歌曲列表 ===\n");
for (int i = 0; i < list.total; i++) {
printf("%d. %s\n", i, strrchr(list.name[i], '/') + 1);
// strrchr找到最后一个'/',+1得到文件名
}
// 3. 获取用户选择
printf("输入编号选择歌曲 (0-%d): ", list.total - 1);
char choose[10] = {0};
fgets(choose, sizeof(choose), stdin);
int num = atoi(choose); // 字符串转整数
// 4. 验证并播放
if (num >= 0 && num < list.total) {
list.current = num; // 更新当前索引
do_play(list.name[num]); // 播放选中的文件
return 0; // 成功
} else {
printf("无效的选择\n");
return -1; // 失败
}
}
do_prev() 和 do_next():歌曲切换
// 上一首:循环算法
list.current = (list.current - 1 + list.total) % list.total;
// 下一首:循环算法
list.current = (list.current + 1) % list.total;
循环算法原理:
-
% list.total:确保索引在0到total-1范围内 -
+ list.total:处理负索引情况
3.5 高级控制函数
do_speed():速度控制
int do_speed()
{
// 显示子菜单
printf("\n=== 播放速度控制 ===\n");
printf("1. 加速 1.5x\n");
printf("2. 减速 0.5x\n");
printf("3. 恢复正常速度\n");
printf("输入选择: ");
// 获取选择并发送相应命令
char input[10];
fgets(input, sizeof(input), stdin);
int choice = atoi(input);
switch (choice) {
case 1: send_cmd("speed_set 1.5"); break; // 1.5倍速
case 2: send_cmd("speed_set 0.5"); break; // 0.5倍速
case 3: send_cmd("speed_set 1.0"); break; // 正常速度
default: printf("无效选项\n");
}
return 0;
}
MPlayer速度命令:
-
speed_set 1.5:设置为1.5倍速度 -
speed_set 0.5:设置为0.5倍速度 -
speed_set 1.0:恢复正常速度
do_seek():快进快退
int do_seek()
{
printf("\n=== 快进快退 ===\n");
printf("1. 前进10秒\n");
printf("2. 后退10秒\n");
printf("3. 前进30秒\n");
printf("4. 后退30秒\n");
printf("输入选择: ");
char input[10];
fgets(input, sizeof(input), stdin);
int choice = atoi(input);
switch (choice) {
case 1: send_cmd("seek 10"); break; // 前进10秒
case 2: send_cmd("seek -10"); break; // 后退10秒
case 3: send_cmd("seek 30"); break; // 前进30秒
case 4: send_cmd("seek -30"); break; // 后退30秒
default: printf("无效选项\n");
}
return 0;
}
MPlayer跳转命令:
-
seek +N:向前跳转N秒 -
seek -N:向后跳转N秒
3.6 资源管理函数
cleanup():清理资源
void cleanup()
{
printf("\n正在清理资源...\n");
// 1. 发送退出命令(如果管道已打开)
if (fifo_fd >= 0) {
send_cmd("quit"); // 告诉MPlayer退出
close(fifo_fd); // 关闭管道
fifo_fd = -1; // 标记为未打开
}
// 2. 终止MPlayer进程(如果存在)
if (mplayer_pid > 0) {
kill(mplayer_pid, SIGTERM); // 发送终止信号
waitpid(mplayer_pid, NULL, 0); // 等待进程结束
mplayer_pid = 0; // 清除记录
}
// 3. 删除管道文件
unlink("/tmp/fifo"); // 删除命名管道
}
清理顺序:
-
发送退出命令(优雅退出)
-
关闭管道连接
-
终止进程(强制退出)
-
删除管道文件
handle():信号处理
void handle(int sig)
{
printf("\n收到信号 %d,正在退出...\n", sig);
cleanup(); // 清理资源
exit(0); // 退出程序
}
处理的信号:
-
SIGINT(2):Ctrl+C中断 -
SIGTERM(15):终止信号
3.7 主函数 main()
int main(int argc, char **argv)
{
// 1. 设置信号处理
signal(SIGINT, handle); // Ctrl+C处理
signal(SIGTERM, handle); // 终止信号处理
// 2. 初始化系统
do_init();
// 3. 自动播放第一首(如果有文件)
if (list.total > 0) {
printf("找到 %d 个媒体文件\n", list.total);
printf("自动播放第一首歌曲...\n");
do_play(list.name[0]); // 播放索引0的文件
sleep(1); // 等待MPlayer启动
} else {
printf("在 %s 目录中没有找到媒体文件\n", MEDIA_PATH);
printf("支持格式: .mp4, .mp3, .flv\n");
}
// 4. 主循环(用户交互)
while (1) {
show_menu(); // 显示菜单
char input[256] = {0};
fgets(input, sizeof(input), stdin); // 读取用户输入
input[strcspn(input, "\n")] = 0; // 去除换行符
int choice = atoi(input); // 转换为整数
// 根据选择调用相应功能
switch (choice) {
case 1: do_list(); break; // 列表选择
case 2: do_pause(); break; // 暂停/继续
case 3: do_stop(); break; // 停止播放
case 4: do_prev(); break; // 上一首
case 5: do_next(); break; // 下一首
case 6: do_speed(); break; // 速度控制
case 7: do_seek(); break; // 快进快退
case 8: do_mode(); break; // 播放模式
case 9: // 退出程序
cleanup();
printf("再见!\n");
return 0;
default: // 无效输入
printf("无效选项,请重新输入\n");
}
}
return 0;
}
主流程:
-
初始化
-
自动播放
-
菜单循环
-
退出清理
四、MPlayer命令参考
| 命令 | 格式 | 说明 | 程序中的使用 |
|---|---|---|---|
| pause | pause |
暂停/继续切换 | send_cmd("pause") |
| stop | stop |
停止播放 | send_cmd("stop") |
| quit | quit |
退出MPlayer | send_cmd("quit") |
| seek | seek ±N |
跳转N秒 | send_cmd("seek 10") |
| speed_set | speed_set X |
设置速度X倍 | send_cmd("speed_set 1.5") |
| volume | volume ±N |
调整音量 | 未使用 |
五、程序运行流程
5.1 启动流程
main()
├── signal() # 设置信号处理器
├── do_init() # 初始化
│ ├── 扫描目录 # 查找媒体文件
│ └── 创建管道 # /tmp/fifo
├── 自动播放第一首 # 如果有文件
└── 进入主循环 # 用户交互
5.2 播放流程
do_play("文件路径")
├── 停止当前MPlayer(如果存在)
│ ├── send_cmd("quit")
│ ├── kill(SIGTERM)
│ └── waitpid()
├── 关闭旧管道
├── fork()创建子进程
│ └── execlp()启动MPlayer
└── 显示播放信息
5.3 命令发送流程
send_cmd("命令")
├── 检查管道是否打开
├── 打开管道(如果需要)
├── 格式化命令(添加\n)
├── write()发送命令
└── 错误处理
六、代码亮点分析
6.1 优点
-
健壮的错误处理:区分致命错误和非致命错误
-
资源管理完善:正确清理进程和文件
-
用户友好:中文界面,清晰提示
-
代码复用 :
send_cmd()统一处理命令发送
6.2 潜在问题
-
扩展名检查不严谨:只检查最后3个字符
-
缓冲区溢出风险 :使用
sprintf而非snprintf -
固定数组大小:最多10个文件,路径最长512字节
-
缺少MPlayer状态检查:假设MPlayer总是能正常启动
6.3 改进建议
// 1. 安全字符串处理
snprintf(buf, sizeof(buf), "%s\n", cmd);
// 2. 更严谨的扩展名检查
char *ext = strrchr(filename, '.');
if (ext && (strcasecmp(ext, ".mp3") == 0 || ...))
// 3. 动态内存分配
char **name = malloc(max_files * sizeof(char*));
for (i = 0; i < max_files; i++) {
name[i] = malloc(max_path * sizeof(char));
}
七、学习要点总结
7.1 核心技术点
-
进程创建与管理:fork()、execlp()、waitpid()
-
进程间通信:命名管道(FIFO)
-
信号处理:signal()、SIGINT、SIGTERM
-
文件系统操作:目录遍历、文件过滤
7.2 编程实践
-
模块化设计:功能分离,职责单一
-
错误处理策略:分级处理,合理退出
-
资源管理:谁申请谁释放原则
-
用户交互设计:清晰的菜单和提示
7.3 系统编程概念
-
进程生命周期管理
-
IPC机制应用
-
信号与异常处理
-
文件描述符管理