项目名称:基于Linux终端的Mplayer媒体播放器控制系统
核心功能:
seek %d 1
绝对定位模式,确保每次跳转都基于视频起始时间。例如输入seek 120
将精准定位到2分0秒,避免相对定位模式(seek %d 2
)因多次偏移导致的累计误差问题。此设计保证了进度控制的可靠性,符合用户对"跳转到指定时间"的功能预期。- **Main模块**:负责程序的整体流程控制,包括初始化、主循环和资源释放等。
- **界面渲染模块**:处理菜单的显示和更新,根据用户的操作调整菜单的焦点位置并高亮显示当前选项。
- **输入处理模块**:捕获用户的键盘输入,解析按键事件,并将相应的命令传递给其他模块。
- **播放控制模块**:管理播放器的启动、停止和状态切换,通过FIFO管道向mplayer发送控制指令。
- **进程管理**:创建子进程运行mplayer,并处理子进程的生命周期。
- **FIFO通信**:建立和维护FIFO管道,确保与mplayer之间的稳定通信。
- **播放模式算法**:根据用户选择的播放模式执行相应的逻辑,如顺序播放、单曲循环或随机播放。
typedef enum { // 播放模式枚举
CYCLE_ORDER, // 顺序循环
CYCLE_SINGLE, // 单曲循环
CYCLE_RANDOM // 随机播放
} VIDEO_MOD;
为了实现非阻塞式的键盘输入,程序通过修改`termios`结构来调整终端的行为。具体来说,关闭了规范模式和回显功能,并设置了最小字符数为0,从而允许程序在没有完整行输入的情况下读取字符。
// 通过修改termios结构实现
newt.c_lflag &= ~(ICANON | ECHO); // 关闭规范模式&回显
newt.c_cc[VMIN] = 0; // 非阻塞读取
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
可能问题:
"程序崩溃后终端可能保持异常状态,如何解决?"
技术实现:
// 改进后的终端设置
struct termios oldt;
tcgetattr(STDIN_FILENO, &oldt); // 保存原始设置
atexit(reset_terminal); // 注册退出处理
void reset_terminal() {
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
}
tcsetattr
恢复atexit
注册的函数恢复SIGSEGV
等信号处理,确保异常退出前执行恢复界面渲染模块使用ANSI转义码来实现菜单项的高亮显示。例如,当某个菜单项被选中时,会使用特定的颜色组合(如黑色前景和黄色背景)来突出显示该选项。
采用\033[30;43m
实现黄底黑字的高亮效果,关键要素包含:
%2d
保证菜单序号对齐,%-20s
左对齐字符串并预留扩展空间focus
变量跟踪当前选中项,确保高亮位置与用户操作实时对应// ANSI转义码实现高亮菜单
printf("\033[30;43m %-20s\033[0m\n",c[i]);
// 30:黑色前景 43:黄色背景
播放控制模块通过`fork()`函数创建子进程来启动mplayer,并使用`execlp()`函数加载播放器程序及其参数。为了避免僵尸进程的产生,程序注册了信号处理函数来忽略子进程终止信号。
进程管理细节
问题:
"如何防止mplayer成为僵尸进程?signal(SIGCHLD,SIG_IGN)
的具体作用是什么?"
应答:
// 代码片段
signal(SIGCHLD, SIG_IGN);
SIG_IGN
使内核自动回收子进程资源,避免僵尸进程累积waitpid
方案:非阻塞方式更适合实时交互场景SIGCHLD
忽略)输出重定向必要性:
在播放子进程中使用dup2
重定向标准输出的原因:
int null_fd = open("/dev/null", O_WRONLY);
dup2(null_fd, STDOUT_FILENO); // 丢弃标准输出
dup2(null_fd, STDERR_FILENO); // 丢弃错误输出
自动续播逻辑:
1、亮点:
player_pid
:用于跟踪当前播放进程的PID。在创建新进程时更新,并在进程结束时重置为-1。waitpid
:使用WNOHANG
参数,确保主循环不会被阻塞,可以继续处理其他任务。usleep
控制检查频率,避免过多消耗CPU资源。2、实现逻辑
实现逻辑:
* 1. 如果已有播放进程,先终止旧进程
* 2. 构建完整文件路径
* 3. 创建子进程运行mplayer播放器
* 4. 父进程记录新进程PID
* @brief 自动续播检查函数
*
* 实现逻辑:
* 1. 非阻塞方式检查播放进程状态
* 2. 如果检测到播放结束,根据播放模式续播
* 3. 支持三种播放模式:
* - 顺序播放:播放下一首
* - 随机播放:生成不重复的随机序号
* - 单曲循环:重复播放当前歌曲
*/
系统流程图解:
启动播放
│
▼
[Play函数]
├─▶ 终止旧进程
├─▶ 创建子进程播放
└─▶ 记录新PID
│
▼
主循环开始
│
▼
[check_autoplay]
├─▶ 非阻塞检查进程状态
├─▶ 进程存活 → 继续轮询
└─▶ 进程结束 → 处理续播
│ ├─顺序模式 → 下一首
│ ├─随机模式 → 新随机序号
│ └─单曲循环 → 重复播放
│
▼
[100ms延迟] → 继续循环
状态管理三重奏
player_pid
:实时跟踪播放进程cnt
:记录当前播放位置menu_flag
:控制界面状态安全播放控制
if (player_pid != -1) {
kill(player_pid, SIGTERM); // 确保单进程播放
}
智能防重复机制
do {
cnt = rand() % FIFO_NUM;
} while(cnt == last && FIFO_NUM > 1);
为了与mplayer进行通信,程序创建了一个名为mplayer_ctrl的命名管道,并通过打开该管道进行写入操作来发送控制指令。例如,发送“pause”命令可以让播放器暂停当前播放。
int fd = open(FIFO_PATH,O_WRONLY);
write(fd,"pause\n",6); // 发送控制指令
FIFO管道实现细节
问题:
"为什么选择O_NONBLOCK
模式打开FIFO?如何保证命令的可靠传输?"
应答:
int fd = open(FIFO_PATH, O_WRONLY | O_NONBLOCK);
write(fd, "pause\n", 6)
实际写入6字节随机模式优化:
在随机播放模式下,为了防止连续播放同一首歌曲,程序引入了一个简单的防重复机制。它会在生成新的随机索引之前检查是否与上一次相同,如果相同则重新生成,直到找到一个不同的索引为止。
do{
new_rand = rand()%FIFO_NUM;
attempts++;
}while(new_rand == last_rand && attempts < 3); // 避免连续重复
实现代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include //关于终端输入输出设置的头文件
#include
#define MAX_FILES 100
#define MAX_NAME_LEN 256
#define FIFO_PATH "./mplayer_ctrl"
#define MEDIA_DIR "./media/"
#define FIFO_NUM 7
//自定义按键宏定义
#define KEY_ESC 0x1B
#define KEY_ENTER 0x1A
#define KEY_UP 0x1B5B41//esc[a的十六进制组合值
#define KEY_DOWN 0x1B5B42//esc[b的十六进制组合值
typedef enum
{
CYCLE_ORDER,//顺序循环播放
CYCLE_SINGLE,//单曲循环播放
CYCLE_RANDOM,//随机播放
}VIDEO_MOD;
char medialist[9][1024] =
{
{"查看播放列表"},
{"开始|暂停"},
{"停止播放"},
{"上一个"},
{"下一个"},
{"快进"},
{"播放定位"},
{"播放方式"},
{"退出软件"},
};
int pos_start = 0;
int pos_end=9;
int focus = 0; //选择的焦点
int cnt = 0; //记录高亮时的位置
int menu_flag = 1;// 先打印第一个目录
static int speed_state = 0; //速度状态标示 0:1x 1:2x 2:3x
static int playmode_state = 0; // 播放模式状态标识 0:顺序 1:单曲 2:随机
static pid_t Play_pid = -1;//全局记录播放进程pid -1表示无播放进程
VIDEO_MOD MODING = CYCLE_ORDER;//默认为顺序播放模式
//存储从指定目录中读取的文件名
char c[MAX_FILES][MAX_NAME_LEN];
int getch(void)
{
struct termios oldt, newt;
int ch;
int esc_mode = 0;
unsigned long key = 0;
//获取终端属性信息
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
//设置非阻塞模式
newt.c_lflag &= ~(ICANON | ECHO);
newt.c_cc[VMIN] = 0;//读取最小字符
newt.c_cc[VTIME] = 1;//等待时间10/1秒
//修改new中的ECHO和ICANON参数,使得new为不回显输入内容
//设置终端信息
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
//组合多字节按键
for(int i = 0;i < 3;i++)
{
ch = getchar();
if(ch == EOF)break;
key = (key << 8)|(ch & 0xFF);
}
//用完之后,恢复原来的终端属性
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
switch(key){
case 0x1B5B41: return KEY_UP;
case 0x1B5B42: return KEY_DOWN;
case 0x1B: return KEY_ESC;
case 0x0A: return KEY_ENTER;
default: return (key&0xFF);//返回首个有效字符
}
}
void Play(int i)
{
// 终止现有播放进程(如果有)
if(Play_pid != -1){
kill(Play_pid,SIGTERM);//发送信号
Play_pid = -1;
}
pid_t pid = fork();
if(pid == -1)
{
perror("fork fail");
return;
}
char filepath[PATH_MAX];//用于存储文件的完整路径
//构建完整文件路径
snprintf(filepath,sizeof(filepath),"%s%s",MEDIA_DIR,c[i]);
if(pid == 0)
{
mkfifo(FIFO_PATH,0777);
int null_fd = open("/dev/null",O_WRONLY);//打开“黑洞”文件,写入的任何数据都会被系统丢弃,读取它时则通常立即返回文件结束(EOF)
dup2(null_fd,STDOUT_FILENO);//丢弃标准输出
dup2(null_fd,STDERR_FILENO);//丢弃标准错误
close(null_fd);
execlp("mplayer","mplayer","-slave","-input","file=" FIFO_PATH,filepath,NULL);
perror("up mplayer error\n");
_exit(EXIT_FAILURE);// 退出子进程,返回失败状态
}else{
Play_pid = pid;//记录新进程pid
}
cnt = i;//更新当前播放索引
menu_flag = 1;//回到主菜单
}
void View_two(void)
{
DIR *dir = opendir("./media/");
int i = 0;
if(dir == NULL)
{
perror("opendir fail");
return;
}
struct dirent *pdir;
while((pdir = readdir(dir)) != NULL && i < MAX_FILES)
{
if(pdir->d_name[0] != '.')//跳过隐藏目录
{
strncpy(c[i],pdir->d_name,MAX_NAME_LEN);
c[i][MAX_NAME_LEN-1] = '\0';//确保终止符
i++;
}
}
putchar('\n');
closedir(dir);
printf("+-------------------------------+\n");
printf("| 音视频播放器 |\n");
printf("|-------------------------------|\n");
for(i = pos_start;i < pos_end;i++)
{
if(i == focus)//焦点行显示
{
cnt = focus; //记录当前选择
printf("| |\r|\033[30;43m %-20s\033[0m\n",c[i]);//焦点行高亮
}else //其他行正常显示
{
printf("| |\r|%s\n",c[i]);
}
}
/* 绘制界面底部 */
printf("| |\n");
printf("| |\n");
printf("| |\n");
printf("+-------------------------------+\n");
}
void View(void)
{
int i = 0;
if(menu_flag == 1)
{
printf("+-------------------------------+\n");
printf("| 音视频播放器 |\n");
printf("|-------------------------------|\n");
for(i = pos_start;i < pos_end;i++)
{
if(i == focus)
{
printf("| |\r|\033[30;43m%2d.%-20s\033[0m\n",i+1,medialist[i]);
}else{
printf("| |\r|%2d.%-20s\n",i+1,medialist[i]);
}
}
printf("| |\n");
printf("+-------------------------------+\n");
}else if(menu_flag == 2)
{
View_two();
}
return;
}
//停止播放
void Stopplay(void)
{
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);//设置非阻塞模式
if(fd == -1){
perror("open fail");
return;
}
write(fd,"stop\n",5);//往管道文件中写入暂停指令
close;
return;
}
void Pause(void)//暂停
{
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);//设置非阻塞模式
if(fd == -1){
perror("open fail");
return;
}
write(fd,"pause\n",6);//往管道文件中写入暂停指令
close;
return;
}
//播放上一个
void last()
{
if(MODING == CYCLE_ORDER)//顺序循环播放
{
if(cnt - 1 >= 0)//检查cnt是否大于0,避免减到负数
{
Stopplay();
cnt--;
Play(cnt);
}
}else if(MODING == CYCLE_SINGLE)//单曲循环
{
Stopplay();
Play(cnt);
}else if(MODING == CYCLE_RANDOM)
{
srand(time(NULL));
Stopplay();
int tmp = rand() % FIFO_NUM;//生成0~最大歌曲数的随机数
Play(tmp);
}
}
//播放下一个
void next()
{
switch(MODING){
case CYCLE_ORDER:{
if(cnt +1 < FIFO_NUM)//检查下一首歌曲是否超出最大歌曲数,避免超出到歌曲边界
{
Stopplay();
cnt++;
Play(cnt);
}else{
printf("已到列表末尾,从第一首重新开始\n");
sleep(1);//防止被清屏函数清除
Stopplay();
cnt = 0;
Play(cnt);
}
break;}
case CYCLE_SINGLE:{
Stopplay();
Play(cnt);
break; }
case CYCLE_RANDOM:{
int last_rand = cnt;// 保存当前播放索引
int attempts = 0;//随机重复计数器
int new_rand;
// srand(time(NULL));
do{
new_rand = rand()%FIFO_NUM;// 使用全局初始化的随机种子
attempts++;
}while(new_rand == last_rand && attempts < 3);//避免多次重复且最多尝试三次
last_rand = cnt;//更新最后播放记录
cnt = new_rand;//更新当前索引
Stopplay();
Play(cnt);
break;
}
}
}
//一倍速
void speed1(void)
{
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);//设置非阻塞模式
if(fd == -1){
perror("open fail");
return;
}
write(fd,"speed_set 1\n",12);
close(fd);
return;
}
//二倍速
void speed2(void)
{
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);//设置非阻塞模式
if(fd == -1){
perror("open fail");
return;
}
write(fd,"speed_set 2\n",12);
close(fd);
return;
}
//三倍速
void speed3(void)
{
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);//设置非阻塞模式
if(fd == -1){
perror("open fail");
return;
}
write(fd,"speed_set 3\n",12);
close(fd);
return;
}
void Playlocation()
{
int sec = 0;
printf("Input palylocation(s):");
while(1){
if(scanf("%d",&sec) != 1){
printf("Input error\n");
while(getchar() != '\n');
continue;
}
if(sec >= 0)
{
// 清除输入缓冲区中的剩余字符,包括换行符,防止阻塞
while(getchar()!='\n');
break;
}
printf("时间为负,重新输入:");
}
char buf[64];
snprintf(buf,sizeof(buf),"seek %d 1\n",sec);//使用绝对定位模式
int fd = open(FIFO_PATH,O_WRONLY | O_NONBLOCK);
if(fd == -1){
perror("open fail");
return;
}
write(fd,buf,strlen(buf));
close(fd);
}
void Sequential_cycle_mode()
{
printf("顺序循环播放\n");
MODING = CYCLE_ORDER;
sleep(1);
}
void Single_cycle_mode()
{
printf("单曲循环播放\n");
MODING = CYCLE_SINGLE;
sleep(1);
}
void Random_cycle_mode()
{
printf("随机播放\n");
MODING = CYCLE_RANDOM;
sleep(1);
}
void update_play_mode(void)
{
switch(playmode_state) {
case 0:
Sequential_cycle_mode();
break;
case 1:
Single_cycle_mode();
break;
case 2:
Random_cycle_mode();
break;
}
}
void MenuChoose(void)
{
int key = getch();
//ESC按键处理
if(key == KEY_ESC){
if(menu_flag == 1){
Stopplay();
exit(0);//当处于一级菜单时退出
}else if(menu_flag == 2){
menu_flag = 1;//切换到菜单一
}
return;
}
//方向键处理
if(menu_flag == 1){
switch(key){
case KEY_UP:
if(focus > pos_start){//如果此时焦点不在第一首歌
focus--;
}
break;
case KEY_DOWN:
if(focus < pos_end){//如果此时焦点不在最后一首歌
focus++;
}
break;
case KEY_ENTER:
switch(focus){
case 0:
menu_flag = 2;//进入二级菜单
break;
case 1:
Pause();
break;
case 2:
Stopplay();
break;
case 3:
last();
break;
case 4:
next();
break;
case 5:
speed_state = (speed_state+1)%3;
switch(speed_state){
case 0:
speed1();
break;
case 1:
speed2();
break;
case 2:
speed3();
break;
}
break;
case 6:
Playlocation();
break;
case 7:
playmode_state = (playmode_state +1) % 3;
update_play_mode();
break;
case 8:
Stopplay();
exit(0);
break;
}
break;
}
}else if(menu_flag == 2){
switch(key){
case KEY_UP:
if(focus > pos_start){//如果此时焦点不在第一首歌
focus--;
}
break;
case KEY_DOWN:
if(focus < pos_end){//如果此时焦点不在最后一首歌
focus++;
}
break;
case KEY_ENTER:
switch(focus){
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
Play(focus);
break;
}
}
}
}
void check_autoplay(){
if(Play_pid == -1)return;//如果没有播放进程直接返回
int status;
//非阻塞检查进程状态
if(waitpid(Play_pid,&status,WNOHANG) > 0){//检测到进程结束
Play_pid = -1;//重置播放状态
//根据播放模式续播
switch(MODING){
case CYCLE_ORDER:
next();
break;
case CYCLE_RANDOM:{
int last = cnt;
do{
cnt = rand()%FIFO_NUM;
}while(cnt == last && FIFO_NUM >1);
Play(cnt);
break;
}
case CYCLE_SINGLE:
Play(cnt);
break;
}
}
}
int main(int argc, const char *argv[])
{
while(1)
{
system("clear");
View();
MenuChoose();
check_autoplay();
usleep(50000);//每50ms检查播放状态
}
return 0;
}