基于Linux终端的Mplayer媒体播放器控制系统

一、项目概述

项目名称:基于Linux终端的Mplayer媒体播放器控制系统
核心功能

  1. 多级菜单交互(主菜单/播放列表):系统提供主菜单和播放列表两个层级的菜单,用户可以通过方向键在菜单项之间导航,并使用Enter键选择操作。这种设计使得用户能够方便地浏览和选择不同的功能。
  2. 播放控制(播放/暂停/停止/上下曲目):支持基本的播放控制功能,包括播放、暂停、停止以及上下曲目的切换。此外,还提供了倍速播放(如1x、2x、3x)和播放进度定位的功能,以满足不同用户的个性化需求。
  3. 播放模式切换(顺序/单曲/随机):用户可以在顺序播放、单曲循环和随机播放三种模式之间自由切换,增加了播放的灵活性和多样性。
  4. 倍速播放(1x/2x/3x)
  5. 播放进度定位    绝对定位模式选择:在播放进度定位功能中,采用seek %d 1绝对定位模式,确保每次跳转都基于视频起始时间。例如输入seek 120将精准定位到2分0秒,避免相对定位模式(seek %d 2)因多次偏移导致的累计误差问题。此设计保证了进度控制的可靠性,符合用户对"跳转到指定时间"的功能预期。
  6. 非阻塞式键盘输入响应:通过修改终端属性,实现了对键盘输入的实时响应,确保了用户操作的流畅性。

技术亮点

  • 终端界面控制(ANSI转义码实现高亮菜单)
  • 进程间通信(FIFO管道控制mplayer)
  • 多播放模式算法实现
  • 终端属性实时修改(非阻塞输入)

二、系统架构

1. 模块划分

- **Main模块**:负责程序的整体流程控制,包括初始化、主循环和资源释放等。
- **界面渲染模块**:处理菜单的显示和更新,根据用户的操作调整菜单的焦点位置并高亮显示当前选项。
- **输入处理模块**:捕获用户的键盘输入,解析按键事件,并将相应的命令传递给其他模块。
- **播放控制模块**:管理播放器的启动、停止和状态切换,通过FIFO管道向mplayer发送控制指令。
  - **进程管理**:创建子进程运行mplayer,并处理子进程的生命周期。
  - **FIFO通信**:建立和维护FIFO管道,确保与mplayer之间的稳定通信。
  - **播放模式算法**:根据用户选择的播放模式执行相应的逻辑,如顺序播放、单曲循环或随机播放。

2. 关键数据结构

typedef enum {          // 播放模式枚举
    CYCLE_ORDER,        // 顺序循环
    CYCLE_SINGLE,       // 单曲循环  
    CYCLE_RANDOM        // 随机播放
} VIDEO_MOD;


三、核心实现解析

1. 终端控制关键技术

非阻塞输入


为了实现非阻塞式的键盘输入,程序通过修改`termios`结构来调整终端的行为。具体来说,关闭了规范模式和回显功能,并设置了最小字符数为0,从而允许程序在没有完整行输入的情况下读取字符。

// 通过修改termios结构实现
newt.c_lflag &= ~(ICANON | ECHO);  // 关闭规范模式&回显
newt.c_cc[VMIN] = 0;              // 非阻塞读取
tcsetattr(STDIN_FILENO, TCSANOW, &newt);

2. 终端属性恢复

可能问题

"程序崩溃后终端可能保持异常状态,如何解决?"

技术实现

// 改进后的终端设置
struct termios oldt;
tcgetattr(STDIN_FILENO, &oldt); // 保存原始设置
atexit(reset_terminal);         // 注册退出处理

void reset_terminal() {
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
}
  • 双重保障机制
    1. 正常退出时通过tcsetattr恢复
    2. 异常退出时通过atexit注册的函数恢复
  • 信号捕获:添加SIGSEGV等信号处理,确保异常退出前执行恢复

3.界面渲染

界面渲染模块使用ANSI转义码来实现菜单项的高亮显示。例如,当某个菜单项被选中时,会使用特定的颜色组合(如黑色前景和黄色背景)来突出显示该选项。

        采用\033[30;43m实现黄底黑字的高亮效果,关键要素包含:

  • 格式控制%2d保证菜单序号对齐,%-20s左对齐字符串并预留扩展空间
  • 状态同步:通过focus变量跟踪当前选中项,确保高亮位置与用户操作实时对应
// ANSI转义码实现高亮菜单
printf("\033[30;43m %-20s\033[0m\n",c[i]); 
// 30:黑色前景 43:黄色背景

4. 播放控制实现

进程管理

播放控制模块通过`fork()`函数创建子进程来启动mplayer,并使用`execlp()`函数加载播放器程序及其参数。为了避免僵尸进程的产生,程序注册了信号处理函数来忽略子进程终止信号。

 进程管理细节

问题
"如何防止mplayer成为僵尸进程?signal(SIGCHLD,SIG_IGN)的具体作用是什么?"

应答

// 代码片段
signal(SIGCHLD, SIG_IGN); 
  • 僵尸进程防护
    • SIG_IGN使内核自动回收子进程资源,避免僵尸进程累积
    • 对比waitpid方案:非阻塞方式更适合实时交互场景
  • 异常处理
    • mplayer崩溃时父进程能继续响应(通过SIGCHLD忽略)
    • 重启机制:下次播放操作自动创建新进程

输出重定向必要性
在播放子进程中使用dup2重定向标准输出的原因:

  1. 界面纯净性:防止mplayer的输出信息污染父进程的终端界面
  2. 进程稳定性:避免子进程因输出缓冲区满导致阻塞(常见于长时间播放场景)
  3. 资源管理:关闭未使用的文件描述符,防止句柄泄漏

int null_fd = open("/dev/null", O_WRONLY);
dup2(null_fd, STDOUT_FILENO);  // 丢弃标准输出
dup2(null_fd, STDERR_FILENO);  // 丢弃错误输出

自动续播逻辑:

1、亮点:

  1. 全局变量player_pid:用于跟踪当前播放进程的PID。在创建新进程时更新,并在进程结束时重置为-1。
  2. 非阻塞的waitpid:使用WNOHANG参数,确保主循环不会被阻塞,可以继续处理其他任务。
  3. 播放模式处理:根据不同的模式(顺序、随机、单曲循环)选择下一首歌曲的逻辑。
  4. 轮询间隔:通过usleep控制检查频率,避免过多消耗CPU资源。

2、实现逻辑

实现逻辑:
 * 1. 如果已有播放进程,先终止旧进程
 * 2. 构建完整文件路径
 * 3. 创建子进程运行mplayer播放器
 * 4. 父进程记录新进程PID
* @brief 自动续播检查函数
 * 
 * 实现逻辑:
 * 1. 非阻塞方式检查播放进程状态
 * 2. 如果检测到播放结束,根据播放模式续播
 * 3. 支持三种播放模式:
 *    - 顺序播放:播放下一首
 *    - 随机播放:生成不重复的随机序号
 *    - 单曲循环:重复播放当前歌曲
 */

系统流程图解:

启动播放
  │
  ▼  
[Play函数]
  ├─▶ 终止旧进程
  ├─▶ 创建子进程播放
  └─▶ 记录新PID
  │
  ▼  
主循环开始
  │
  ▼  
[check_autoplay]
  ├─▶ 非阻塞检查进程状态
  ├─▶ 进程存活 → 继续轮询
  └─▶ 进程结束 → 处理续播
  │    ├─顺序模式 → 下一首
  │    ├─随机模式 → 新随机序号
  │    └─单曲循环 → 重复播放
  │
  ▼  
[100ms延迟] → 继续循环
  1. 状态管理三重奏

    • player_pid:实时跟踪播放进程
    • cnt:记录当前播放位置
    • menu_flag:控制界面状态
  2. 安全播放控制

    if (player_pid != -1) {
        kill(player_pid, SIGTERM); // 确保单进程播放
    }
    
  3. 智能防重复机制

    do {
        cnt = rand() % FIFO_NUM;
    } while(cnt == last && FIFO_NUM > 1);

5.FIFO通信

为了与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);
  • 非阻塞必要性:防止父进程在mplayer未启动时因写阻塞导致界面卡死
  • 可靠性保障
    • 重试机制:关键命令(如播放控制)添加3次重试
    • 错误检测:通过返回值校验确保write(fd, "pause\n", 6)实际写入6字节
  • 管道特性:命名管道自带缓冲区(默认64KB),足以容纳控制命令

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;
}

你可能感兴趣的:(linux,运维,服务器)