FFmpeg入門:最簡單的音視頻播放器

FFmpeg入門:最簡單的音視頻播放器

前兩章,我們已經了解了分別如何構建一個簡單和音頻播放器和視頻播放器。
FFmpeg入門:最簡單的音頻播放器
FFmpeg入門:最簡單的視頻播放器

本章我們將結合上述兩章的知識,看看如何融合成一個完整的音視頻播放器,跟上我的節奏,本章將是咱們后續完成一個完整的音視頻播放器的起點。

整體流程圖

話不多說,先上圖
在這里插入圖片描述
這個圖似乎有點復雜了,這里我會分別將每個模塊拿出來講述,方便大家一步一步分析整個流程。

第一步:初始化

在這里插入圖片描述
我們首先關注整個流程圖的最上面一部分,這部分其實和之前的流程一樣,主要就是將做一些前置的初始化工作:

1:打開文件,獲取文件上下文
2:找到對應的音頻/視頻流,獲取到Codec上下文
3:打開解碼器
4:分配輸出空間緩存,用于后續存儲解碼的輸出數據
5:音頻/視頻幀的格式轉化上下文
6:初始化SDL組件,主要是視頻的播放窗口和音頻播放器

代碼(省略了部分校驗和參數初始化,方便閱讀,原碼見文章末尾):

	/** 初始化函數 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打開視頻文件,獲取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 對文件探測流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 3. 找到對應的 音頻流/視頻流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}// 4. 將 音頻流/視頻流 編碼參數寫入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);// 5. 查找流的編碼器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);// 6. 打開流的編解碼器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音頻輸出信息構建 */audio_output_set(video_state);/** 視頻輸出信息構建 */video_output_set(video_state);//	SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}/** 初始化音頻SDL設備 */SDL_AudioSpec wanted_spec;wanted_spec.freq = audio_param->out_sample_rate;					// 采樣率wanted_spec.format = AUDIO_S16SYS;									// 采樣格式 16bitwanted_spec.channels = audio_param->out_channels;					// 通道數wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples;					// 單幀處理的采樣點wanted_spec.callback = fill_audio;									// 回調函數wanted_spec.userdata = video_state->aCodecCtx;						// 回調函數的參數/** 初始化視頻SDL設備 */SDL_Window*       window = NULL;SDL_Renderer*     renderer = NULL;SDL_Texture*      texture= NULL;/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->height,SDL_WINDOW_SHOWN);/** 渲染 */renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);/** 紋理 */texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打開音頻播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音頻上下文格式轉換swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout,			// 輸出layoutaudio_param->out_sample_fmt,				// 輸出格式audio_param->out_sample_rate,				// 輸出采樣率&video_state->aCodecCtx->ch_layout,			// 輸入layoutvideo_state->aCodecCtx->sample_fmt,			// 輸入格式video_state->aCodecCtx->sample_rate,		// 輸入采樣率0, NULL);swr_init(video_state->swrCtx);// 視頻上下文格式轉換video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width,			// src 寬video_state->vCodecCtx->height,		// src 高video_state->vCodecCtx->pix_fmt,		// src 格式video_param->width,					// dst 寬video_param->height,					// dst 高video_param->pix_fmt,					// dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 開始播放SDL_PauseAudio(0);

第二步:packet隊列寫入

做完準備工作之后,我們就將源文件中的輸入packet都取出來,放入到對應的音頻packet隊列和視頻packet隊列中,方便后續使用。然后然后分別啟動音頻解碼進程和視頻解碼進程同時進行解碼。

  1. 讀出packet,判斷packet類型
  2. 根據類型放入音頻和視頻packet隊列
  3. 創建解碼進程

在這里插入圖片描述

// 循環1: 從文件中讀取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 寫入音頻pkt隊列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 寫入視頻pkt隊列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 創建一個線程并啟動SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);

第三步:音頻解碼+播放

接下來會分別講一下音頻和視頻解碼進程,這兩個進程是同時開始的。

首先音頻解碼進程的步驟可以參考之前的音頻播放器文章。我簡單說一下步驟

  1. 從音頻packet隊列中取出packet
  2. 對packet進行解碼得到frame
  3. 按音頻輸出格式進行swr_convert轉換得到輸出值,并寫入buffer。
  4. SDL音頻播放器通過回調函數從buffer不斷讀取數據播放。

在這里插入圖片描述
代碼如下:

/**音頻線程*/
int audio_thread(void *arg) {/**1. 從packet_queue隊列中取出packet2. 將packet進行解碼3. 寫入到sdl的緩沖區中*/ 	VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime();								// 播放開始時間戳AVPacket 	packet;int 		ret;AVFrame* 	pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 將packet寫入編解碼器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);// 獲取解碼后的幀while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式轉化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d幀 | pts:%lld | 幀大小(采樣點):%d | 實際播放點%.2fs | 預期播放點%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 設置讀取的音頻數據audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 結束video_state->isEnd = 1;return 0;
}

第四步:視頻解碼(子線程)+播放(主線程)

說視頻的解碼和播放之前,先提一點:SDL的主窗口操作是需要在主線程中進行的。因此我們不能再解碼子線程中直接渲染SDL窗口,否則會造成內存泄漏。知道這個知識之后,更能理解接下來的流程分析。

我們視頻解碼播放拆成兩個部分:解碼+播放

第一部分:解碼子線程,在子線程中完成解碼,通過標識符的方式通知到主線程幀已更新,并渲染出來。
第二部分:主線程播放,循環監聽子線程的通知標識,并更新窗口幀進行顯示。

在這里插入圖片描述
代碼如下:

視頻解碼子線程

/**視頻線程*/
int video_thread(void *arg) {/**1. 從視頻pkt隊列中讀出packet2. 送入解碼器解碼并取出3. 使用SDL進行渲染4. 根據pts計算延遲SDL_DELAY*/VideoState* 	video_state = (VideoState*) arg;PacketQueue* 	video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* 		out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime();							// 開始播放時間(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE;		// pts單位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 將packet寫入編解碼器int ret = avcodec_send_packet(pCodecCtx, &packet);// 從解碼器中取出原始幀while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 幀格式轉化,轉為YUV420Psws_scale(video_state->swsCtx,						// sws_context轉換(uint8_t const * const *)pFrame->data,	// 輸入 datapFrame->linesize,							// 輸入 每行數據的大小(對齊)0,										// 輸入 Y軸位置pCodecCtx->height,						// 輸入 heightout_frame->data,							// 輸出 dataout_frame->linesize);						// 輸出 linesize// 幀更新video_state->videoParam->frame_update = 1;// 計算延遲int64_t pts = pFrame->pts;											// ptsint64_t actual_playback_time = av_start_time + pts * frame_delay;	// 實際播放時間int64_t current_time = av_gettime();if (actual_playback_time > current_time) {SDL_Delay((Uint32)(actual_playback_time-current_time)/1000);	// 延遲當前時間和實際播放時間}video_state->videoParam->index++;printf("第%i幀 | 屬于%s | pts為%d | 時長為%.2fms | 實際播放點為%.2fs | 預期播放點為%.2fs\n ",video_state->videoParam->index,get_frame_type(pFrame),(int)pFrame->pts,(double)(av_gettime() - frame_start_time)/1000,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));frame_start_time = av_gettime();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 結束video_state->isEnd = 1;return 1;
}

渲染主線程

while (!video_state->isEnd) {// 處理事件(必須由主線程執行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 將AVFrame的數據寫入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新紋理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0],	// 	Yout_frame->data[1], out_frame->linesize[1],	// 	Uout_frame->data[2], out_frame->linesize[2]);	//  V// 渲染頁面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置標志video_state->videoParam->frame_update = 0;}}

完整代碼

sample_player.h

//
//  sample_player.h
//  learning
//
//  Created by chenhuaiyi on 2025/2/26.
//#ifndef sample_player_h
#define sample_player_h#include <stdio.h>
// ffmpeg
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libavutil/fifo.h"
#include "libavutil/channel_layout.h"
// SDL
#include "SDL.h"
#include "SDL_thread.h"/**宏定義*/
#define USE_SDL 1typedef struct MyAVPacketList {AVPacket 		*pkt;int 			serial;
} MyAVPacketList;/**packet隊列*/
typedef struct PacketQueue {AVFifo* 		pkt_list;		// fifo隊列int 			size;			// 隊列大小SDL_mutex*		mutex;			// 互斥信號量SDL_cond*		cond;			// 條件變量,阻塞線程
} PacketQueue;/**數據類型定義*/
typedef struct AudioInfo{Uint32  		audio_len;		// 緩沖區長度Uint8*			audio_pos;		// 緩沖區起始地址指針
} AudioInfo;/**語音輸出參數*/
typedef struct AudioParam {AVChannelLayout 	out_channel_layout;			// layoutint 				out_nb_samples;				// 每一幀的樣本數enum AVSampleFormat out_sample_fmt;				// 格式int 				out_sample_rate;			// 采樣率int 				out_channels;				// 輸出通道數int					index;						// 音頻幀總數int 				out_buffer_size;			// 音頻輸出緩沖區大小uint8_t* 			out_buffer;					// 音頻輸出緩沖區
} AudioParam;/**視頻輸出參數*/
typedef struct VideoParam {int 				width;						// 寬int 				height;						// 高enum AVPixelFormat	pix_fmt;					// 格式 YUV420Pint 				num_bytes;					// 單幀字節數int					index;						// 視頻幀總數AVFrame*			out_frame;					// 輸出幀int					frame_update;				// 幀更新標識
} VideoParam;/**全局參數*/
typedef struct VideoState {AVFormatContext* 	formatCtx;			// format上下文int				 	audioStream;		// 音頻流索引AVCodecContext*  	aCodecCtx;			// 音頻codec上下文const AVCodec*	 	aCodec;				// 音頻解碼器AudioParam*		 	audioParam;			// 音頻參數SwrContext*			swrCtx;				// 音頻上線文轉換int					videoStream;		// 視頻流索引AVCodecContext*  	vCodecCtx;			// 視頻codec上新聞const AVCodec*	 	vCodec;				// 視頻解碼器VideoParam*			videoParam;			// 視頻參數struct SwsContext* 	swsCtx;				// 視頻上下文轉換PacketQueue*		aQueue;				// 音頻pkt隊列PacketQueue*		vQueue;				// 視頻pkt隊列int					isEnd;				// 結束標志
} VideoState;/**全局變量*/
extern AudioInfo audio_info;#endif /* sample_player_h */

utils.h

//
//  utils.h
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#ifndef utils_h
#define utils_h#include "sample_player.h"int init_video_state(VideoState** video_state);int destory_video_state(VideoState** video_state);int packet_queue_push(PacketQueue* q, AVPacket* pkt);int packet_queue_init(PacketQueue** q, size_t max_size);int packet_queue_pop(PacketQueue* q, AVPacket* pkt);void packet_queue_destroy(PacketQueue** q);char* get_frame_type(AVFrame* frame);#endif /* utils_h */

manager.h

//
//  manager.h
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#ifndef manager_h
#define manager_h#include "sample_player.h"/**音頻輸出信息設置*/
int audio_output_set(VideoState* video_state);/**視頻輸出信息設置*/
int video_output_set(VideoState* video_state);/**音頻SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int));/**視頻SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture);#endif /* manager_h */

utils.c

//
//  utils.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#include "utils.h"/**初始化VideoState*/
int init_video_state(VideoState** video_state) {*video_state = av_malloc(sizeof(VideoState));(*video_state)->formatCtx = avformat_alloc_context();(*video_state)->audioStream = 0;(*video_state)->aCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->audioParam = av_malloc(sizeof(AudioParam));(*video_state)->videoStream = 0;(*video_state)->vCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->videoParam = av_malloc(sizeof(VideoParam));(*video_state)->videoParam->frame_update = 0;/** pkt隊列初始化 */(*video_state)->aQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->aQueue, 1);(*video_state)->vQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->vQueue, 1);(*video_state)->isEnd = 0;return 1;
}/**銷毀VideoState*/
int destory_video_state(VideoState** video_state){swr_free(&(*video_state)->swrCtx);avcodec_free_context(&(*video_state)->aCodecCtx);av_free((*video_state)->audioParam->out_buffer);av_free((*video_state)->audioParam);sws_freeContext((*video_state)->swsCtx);avcodec_free_context(&(*video_state)->vCodecCtx);av_frame_free(&(*video_state)->videoParam->out_frame);av_free((*video_state)->videoParam);/** 隊列釋放 */packet_queue_destroy(&(*video_state)->aQueue);packet_queue_destroy(&(*video_state)->vQueue);if ((*video_state)->formatCtx != NULL) {avformat_close_input(&(*video_state)->formatCtx);(*video_state)->formatCtx = NULL;}av_free(*video_state);return 1;
}/**初始化隊列*/
int packet_queue_init(PacketQueue** q, size_t max_size) {// 創建一個 AVFifo 隊列,每個元素的大小為 sizeof(AVPacket)*q = av_malloc(sizeof(PacketQueue));(*q)->pkt_list = av_fifo_alloc2(max_size, sizeof(MyAVPacketList), AV_FIFO_FLAG_AUTO_GROW);(*q)->size = 0;(*q)->mutex = SDL_CreateMutex();(*q)->cond = SDL_CreateCond();if (!(*q)->pkt_list) {return -1;}return 0;
}/**寫入隊列*/
int packet_queue_push(PacketQueue* q, AVPacket* pkt) {MyAVPacketList pNode;if (!q || !pkt) {return -1;}AVPacket* pkt1 = av_packet_alloc();if (!pkt1) {av_packet_unref(pkt);return -1;}SDL_LockMutex(q->mutex);av_packet_ref(pkt1, pkt);pNode.pkt = pkt1;// 將 pkt 壓入隊列if (av_fifo_write(q->pkt_list, &pNode, 1) < 0) {SDL_UnlockMutex(q->mutex);return -1;}q->size++;SDL_CondSignal(q->cond);SDL_UnlockMutex(q->mutex);return 0;
}/**彈出隊列*/
int packet_queue_pop(PacketQueue* q, AVPacket* pkt) {if (!q || !pkt) {return -1;}SDL_LockMutex(q->mutex);MyAVPacketList pNode;// 從隊列中彈出一個元素, 沒找到則阻塞線程,等待生產者釋放if (av_fifo_read(q->pkt_list, &pNode, 1) < 0) {SDL_CondWait(q->cond, q->mutex);}q->size--;av_packet_move_ref(pkt, pNode.pkt);av_packet_free(&pNode.pkt);SDL_UnlockMutex(q->mutex);return 0;
}/**銷毀隊列*/
void packet_queue_destroy(PacketQueue** q) {if ((*q) && (*q)->pkt_list) {// 釋放隊列中的所有 AVPacketMyAVPacketList pNode;SDL_LockMutex((*q)->mutex);while (av_fifo_read((*q)->pkt_list, &pNode, 1) >= 0) {av_packet_free(&pNode.pkt);  // 釋放 AVPacket 的資源}SDL_UnlockMutex((*q)->mutex);// 釋放 AVFifo 隊列(*q)->size = 0;av_fifo_freep2(&(*q)->pkt_list);SDL_DestroyMutex((*q)->mutex);SDL_DestroyCond((*q)->cond);av_free(*q);}
}/**獲取幀類型*/
char* get_frame_type(AVFrame* frame) {switch (frame->pict_type) {case AV_PICTURE_TYPE_I:return "I";break;case AV_PICTURE_TYPE_P:return "P";break;case AV_PICTURE_TYPE_B:return "B";break;case AV_PICTURE_TYPE_S:return "S";break;case AV_PICTURE_TYPE_SI:return "SI";break;case AV_PICTURE_TYPE_SP:return "SP";break;case AV_PICTURE_TYPE_BI:return "BI";break;default:return "N";break;}
}

manager.c

//
//  manager.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#include "manager.h"/**音頻輸出信息構建*/
int audio_output_set(VideoState* video_state) {AudioParam* audio_param = video_state->audioParam;// 輸出用到的信息av_channel_layout_default(&audio_param->out_channel_layout, 2);audio_param->out_nb_samples = video_state->aCodecCtx->frame_size;			// 編解碼器每個幀需要處理或者輸出的采樣點的大小 AAC:1024  MP3:1152audio_param->out_sample_fmt = AV_SAMPLE_FMT_S16;							// 采樣格式audio_param->out_sample_rate = 44100;										// 采樣率audio_param->out_channels = audio_param->out_channel_layout.nb_channels;	// 通道數// 獲取需要使用的緩沖區大小 -> 通道數,單通道樣本數,位深 1024(單幀處理的采樣點)*2(雙通道)*2(16bit對應2字節)audio_param->out_buffer_size = av_samples_get_buffer_size(NULL, audio_param->out_channels,audio_param->out_nb_samples,audio_param->out_sample_fmt, 1);// 分配緩沖區空間audio_param->out_buffer = NULL;av_samples_alloc(&audio_param->out_buffer, NULL, audio_param->out_channels,audio_param->out_nb_samples, audio_param->out_sample_fmt, 1);return 1;
}/**視頻輸出信息構建*/
int video_output_set(VideoState* video_state) {VideoParam* video_param = video_state->videoParam;// 基礎信息video_param->width = video_state->vCodecCtx->width;video_param->height = video_state->vCodecCtx->height;video_param->pix_fmt = AV_PIX_FMT_YUV420P;// 計算單幀大小, 分配單幀內存video_param->num_bytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, video_param->width, video_param->height, 1);video_param->out_frame = av_frame_alloc();av_image_alloc(video_param->out_frame->data, video_param->out_frame->linesize,video_param->width, video_param->height, AV_PIX_FMT_YUV420P, 1);return 1;
}/**音頻SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int)) {AudioParam* audio_param = video_state->audioParam;wanted_spec->freq = audio_param->out_sample_rate;					// 采樣率wanted_spec->format = AUDIO_S16SYS;									// 采樣格式 16bitwanted_spec->channels = audio_param->out_channels;					// 通道數wanted_spec->silence = 0;wanted_spec->samples = audio_param->out_nb_samples;					// 單幀處理的采樣點wanted_spec->callback = fn;											// 回調函數wanted_spec->userdata = video_state->aCodecCtx;						// 回調函數的參數return 1;
}/**視頻SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture){AVCodecContext* pCodecCtx = video_state->vCodecCtx;/** 窗口 */*window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,pCodecCtx->width,pCodecCtx->height,SDL_WINDOW_SHOWN);if (!*window) {printf("SDL_CreateWindow Error: %s\n", SDL_GetError());SDL_Quit();return 1;}/** 渲染 */*renderer = SDL_CreateRenderer(*window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);if (!*renderer) {printf("SDL_CreateRenderer Error: %s\n", SDL_GetError());SDL_DestroyWindow(*window);SDL_Quit();return 1;}/** 紋理 */*texture = SDL_CreateTexture(*renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);return 1;
}

main.c

//
//  main.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/26.
//#include "utils.h"
#include "manager.h"AudioInfo audio_info;/* udata: 傳入的參數* stream: SDL音頻緩沖區* len: SDL音頻緩沖區大小* 回調函數*/
void fill_audio(void *udata, Uint8 *stream, int len){SDL_memset(stream, 0, len);			// 必須重置,不然全是電音!!!if(audio_info.audio_len==0){					// 有音頻數據時才調用return;}len = (len>audio_info.audio_len ? audio_info.audio_len : len);	// 最多填充緩沖區大小的數據SDL_MixAudio(stream, audio_info.audio_pos, len, SDL_MIX_MAXVOLUME);audio_info.audio_pos += len;audio_info.audio_len -= len;
}/**音頻線程*/
int audio_thread(void *arg) {/**1. 從packet_queue隊列中取出packet2. 將packet進行解碼3. 寫入到sdl的緩沖區中*/ 	VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime();								// 播放開始時間戳AVPacket 	packet;int 		ret;AVFrame* 	pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 將packet寫入編解碼器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);if ( ret < 0 ) {printf("send packet error\n");return -1;}// 獲取解碼后的幀while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式轉化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d幀 | pts:%lld | 幀大小(采樣點):%d | 實際播放點%.2fs | 預期播放點%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 設置讀取的音頻數據audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 結束video_state->isEnd = 1;return 0;
}/**視頻線程*/
int video_thread(void *arg) {/**1. 從視頻pkt隊列中讀出packet2. 送入解碼器解碼并取出3. 使用SDL進行渲染4. 根據pts計算延遲SDL_DELAY*/VideoState* 	video_state = (VideoState*) arg;PacketQueue* 	video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* 		out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime();							// 開始播放時間(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE;		// pts單位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 將packet寫入編解碼器int ret = avcodec_send_packet(pCodecCtx, &packet);if (ret < 0) {printf("packet resolve error!");break;}// 從解碼器中取出原始幀while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 幀格式轉化,轉為YUV420Psws_scale(video_state->swsCtx,						// sws_context轉換(uint8_t const * const *)pFrame->data,	// 輸入 datapFrame->linesize,							// 輸入 每行數據的大小(對齊)0,										// 輸入 Y軸位置pCodecCtx->height,						// 輸入 heightout_frame->data,							// 輸出 dataout_frame->linesize);						// 輸出 linesize// 幀更新video_state->videoParam->frame_update = 1;// 計算延遲int64_t pts = pFrame->pts;											// ptsint64_t actual_playback_time = av_start_time + pts * frame_delay;	// 實際播放時間int64_t current_time = av_gettime();if (actual_playback_time > current_time) {SDL_Delay((Uint32)(actual_playback_time-current_time)/1000);	// 延遲當前時間和實際播放時間}video_state->videoParam->index++;printf("第%i幀 | 屬于%s | pts為%d | 時長為%.2fms | 實際播放點為%.2fs | 預期播放點為%.2fs\n ",video_state->videoParam->index,get_frame_type(pFrame),(int)pFrame->pts,(double)(av_gettime() - frame_start_time)/1000,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));frame_start_time = av_gettime();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 結束video_state->isEnd = 1;return 1;
}int main(int argc, char* argv[])
{VideoState* 		video_state;AudioParam*			audio_param;VideoParam*			video_param;SDL_Event 			event;SDL_Rect      		rect;if(argc < 2) {fprintf(stderr, "Usage: test <file>\n");exit(1);}/** 初始化函數 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打開視頻文件,獲取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 對文件探測流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 打印信息av_dump_format(video_state->formatCtx, 0, argv[1], 0);// 3. 找到對應的 音頻流/視頻流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}if(video_state->audioStream==-1){printf("Didn't find a audio stream.\n");return -1;}if (video_state->videoStream==-1) {printf("Didn't find a video stream.\n");return -1;}// 4. 將 音頻流/視頻流 編碼參數寫入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);
//	avcodec_parameters_free(&aCodecParam); 這個是不需要手動釋放的AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);
//	avcodec_parameters_free(&vCodecParam);// 5. 查找流的編碼器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);if(video_state->aCodec==NULL){printf("Audio codec not found.\n");return -1;}video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);if(video_state->vCodec==NULL){printf("Video codec not found.\n");return -1;}// 6. 打開流的編解碼器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音頻輸出信息構建 */audio_output_set(video_state);/** 視頻輸出信息構建 */video_output_set(video_state);//	SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}// 在 main 函數開始處添加SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, "0");SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");/** 初始化音頻SDL設備 */SDL_AudioSpec wanted_spec;//	audio_sdl_set(video_state, &wanted_spec, fill_audio);wanted_spec.freq = audio_param->out_sample_rate;					// 采樣率wanted_spec.format = AUDIO_S16SYS;									// 采樣格式 16bitwanted_spec.channels = audio_param->out_channels;					// 通道數wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples;					// 單幀處理的采樣點wanted_spec.callback = fill_audio;									// 回調函數wanted_spec.userdata = video_state->aCodecCtx;						// 回調函數的參數/** 初始化視頻SDL設備 */SDL_Window*       window = NULL;SDL_Renderer*     renderer = NULL;SDL_Texture*      texture= NULL;//	video_sdl_set(video_state, &window, &renderer, &texture);/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->height,SDL_WINDOW_SHOWN);if (!window) {printf("SDL_CreateWindow Error: %s\n", SDL_GetError());SDL_Quit();return 1;}/** 渲染 */renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);if (!renderer) {printf("SDL_CreateRenderer Error: %s\n", SDL_GetError());SDL_DestroyWindow(window);SDL_Quit();return 1;}/** 紋理 */texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打開音頻播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音頻上下文格式轉換swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout,			// 輸出layoutaudio_param->out_sample_fmt,				// 輸出格式audio_param->out_sample_rate,				// 輸出采樣率&video_state->aCodecCtx->ch_layout,			// 輸入layoutvideo_state->aCodecCtx->sample_fmt,			// 輸入格式video_state->aCodecCtx->sample_rate,		// 輸入采樣率0, NULL);swr_init(video_state->swrCtx);// 視頻上下文格式轉換video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width,			// src 寬video_state->vCodecCtx->height,		// src 高video_state->vCodecCtx->pix_fmt,		// src 格式video_param->width,					// dst 寬video_param->height,					// dst 高video_param->pix_fmt,					// dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 開始播放SDL_PauseAudio(0);int64_t av_start_time = av_gettime();	// 播放開始時間戳AVPacket* packet = av_packet_alloc();	// packet初始化// 循環1: 從文件中讀取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 寫入音頻pkt隊列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 寫入視頻pkt隊列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 創建一個線程并啟動SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);//	video_thread(video_state);AVFrame* out_frame = NULL;while (!video_state->isEnd) {// 處理事件(必須由主線程執行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 將AVFrame的數據寫入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新紋理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0],	// 	Yout_frame->data[1], out_frame->linesize[1],	// 	Uout_frame->data[2], out_frame->linesize[2]);	//  V// 渲染頁面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置標志video_state->videoParam->frame_update = 0;}}// 打印參數printf("格式: %s\n", video_state->formatCtx->iformat->name);printf("時長: %lld us\n", video_state->formatCtx->duration);printf("音頻持續時長為 %.2f,音頻幀總數為 %d\n", (double)(av_gettime()-av_start_time)/AV_TIME_BASE, audio_param->index);printf("碼率: %lld\n", video_state->formatCtx->bit_rate);printf("編碼器: %s (%s)\n", video_state->aCodecCtx->codec->long_name, avcodec_get_name(video_state->aCodecCtx->codec_id));printf("通道數: %d\n", video_state->aCodecCtx->ch_layout.nb_channels);printf("采樣率: %d \n", video_state->aCodecCtx->sample_rate);printf("單通道每幀的采樣點數目: %d\n", video_state->aCodecCtx->frame_size);printf("pts單位(ms*1000): %.2f\n", av_q2d(video_state->formatCtx->streams[video_state->audioStream]->time_base) * AV_TIME_BASE);// 釋放空間av_packet_free(&packet);#if USE_SDLSDL_CloseAudio();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();
#endifdestory_video_state(&video_state);return 0;
}

結語和展望

終于做完了,恭喜你,完成了一個非常粗糙,而且有很多問題的簡單音視頻播放器。接下來幾期,我們跟著大家一起對這個簡單的播放器進行優化。當然我也是個小萌新,所以一步一步來嘛哈哈。先拋出幾個問題:

  1. 時鐘同步怎么做
  2. 如何邊讀出packet,邊解碼frame并播放
  3. 我們如何對輸出的解碼幀進行轉化

ps. 鼓勵大家閱讀ffplay源碼,所有的問題都能迎刃而解,哈哈哈!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/71414.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/71414.shtml
英文地址,請注明出處:http://en.pswp.cn/web/71414.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

windows 上刪除 node_modules

在 Windows 11 上&#xff0c;你可以通過命令行來刪除 node_modules 文件夾并清除 npm 緩存。以下是具體步驟&#xff1a; 刪除 node_modules 打開命令提示符&#xff08;Command Prompt&#xff09;或終端&#xff08;PowerShell&#xff09;。 導航到項目目錄。你可以使用 …

kotlin的val聲明的變量是常量嗎

kotlin的val聲明的變量是常量嗎 DeepSeek R1 思考 65 秒 思考過程 嗯&#xff0c;用戶問的是Kotlin中的val聲明的變量是不是常量。首先我需要回憶一下Kotlin的基礎知識。記得在Kotlin里&#xff0c;val用來聲明只讀變量&#xff0c;也就是說一旦賦值之后就不能再重新賦值了。…

Linux下學【MySQL】中如何實現:多表查詢(配sql+實操圖+案例鞏固 通俗易懂版~)

每日激勵&#xff1a;“不設限和自我肯定的心態&#xff1a;I can do all things。 — Stephen Curry” 緒論?&#xff1a; 本章是MySQL篇中&#xff0c;非常實用性的篇章&#xff0c;相信在實際工作中對于表的查詢&#xff0c;很多時候會涉及多表的查詢&#xff0c;在多表查詢…

【基礎4】插入排序

核心思想 插入排序是一種基于元素比較的原地排序算法&#xff0c;其核心思想是將數組分為“已排序”和“未排序”兩部分&#xff0c;逐個將未排序元素插入到已排序部分的正確位置。 例如撲克牌在理牌的時候&#xff0c;一般會將大小王、2、A、花牌等按大小順序插入到左邊&…

【Flink銀行反欺詐系統設計方案】3.欺詐的7種場景和架構方案、核心表設計

【Flink銀行反欺詐系統設計方案】3.欺詐的7種場景和架構方案、核心表設計 1. **欺詐場景分類與案例說明**1.1 **大額交易欺詐**1.2 **異地交易欺詐**1.3 **高頻交易欺詐**1.4 **異常時間交易欺詐**1.5 **賬戶行為異常**1.6 **設備指紋異常**1.7 **交易金額突變** 2. **普適性軟…

迷你世界腳本生物接口:Creature

生物接口&#xff1a;Creature 彼得兔 更新時間: 2024-05-22 17:51:22 繼承自 Actor 具體函數名及描述如下: 序號 函數名 函數描述 1 getAttr(...) 生物屬性獲取 2 setAttr(...) 生物屬性設置 3 isAdult(...) 判斷該生物是否成年 4 setOxygenNeed(…

深入理解三色標記、CMS、G1垃圾回收器

三色標記算法 簡介 三色標記算法是一種常見的垃圾收集的標記算法&#xff0c;屬于根可達算法的一個分支&#xff0c;垃圾收集器CMS&#xff0c;G1在標記垃圾過程中就使用該算法 三色標記法&#xff08;Tri-color Marking&#xff09;是垃圾回收中用于并發標記存活對象的核心算…

自動駕駛---不依賴地圖的大模型軌跡預測

1 前言 早期傳統自動駕駛方案通常依賴高精地圖&#xff08;HD Map&#xff09;提供道路結構、車道線、交通規則等信息&#xff0c;可參考博客《自動駕駛---方案從有圖邁進無圖》&#xff0c;本質上還是存在問題&#xff1a; 數據依賴性高&#xff1a;地圖構建成本昂貴&#xf…

Xshell及Xftp v8.0安裝與使用-生信工具050

官網 https://www.xshell.com/zh/free-for-home-school/ XShell & Xftp 詳解 1. XShell 介紹 1.1 XShell 是什么&#xff1f; XShell 是一款強大的 Windows 終端模擬器&#xff0c;主要用于遠程管理 Linux、Unix 服務器。它支持 SSH、Telnet、Rlogin 及 SFTP 協議&…

跨域-告別CORS煩惱

跨域-告別CORS煩惱 文章目錄 跨域-告別CORS煩惱[toc]1-參考網址2-思路整理1-核心問題2-個人思考3-腦洞打開4-個人思考-修正版1-個人思考2-腦洞打開 3-知識整理1-什么是跨域一、同源策略簡介什么是源什么是同源是否是同源的判斷哪些操作不受同源策略限制跨域如何跨域 二、CORS 簡…

PE文件結構詳解(DOS頭/NT頭/節表/導入表)使用010 Editor手動解析notepad++.exe的PE結構

一&#xff1a;DOS部分 DOS部分分為DOS MZ文件頭和DOS塊&#xff0c;其中DOS MZ頭實際是一個64位的IMAGE_DOS——HEADER結構體。 DOS MZ頭部結構體的內容如下&#xff0c;我們所需要關注的是前面兩個字節&#xff08;e_magic&#xff09;和后面四個字節&#xff08;e_lfanew&a…

Node JS 調用模型Xenova_all-MiniLM-L6-v2實戰

本篇通過將句子數組轉換為句子的向量表示&#xff0c;并通過平均池化和歸一化處理&#xff0c;生成適合機器學習或深度學習任務使用的特征向量為例&#xff0c;演示通過NodeJS 的方式調用Xenova/all-MiniLM-L6-v2 的過程。 關于 all-MiniLM-L6-v2 的介紹&#xff0c;可以參照上…

【C++學習篇】智能指針

目錄 1. 智能指針的使用場景分析 2. RAII和智能指針的設計思路 3. C標準庫智能指針的使用 4.shared_ptr和weak_ptr 4.1shared_ptr的循環引用問題 4.2 weak_ptr 1. 智能指針的使用場景分析 下?程序中我們可以看到&#xff0c;new了以后&#xff0c;我們也delete了&#xff0c…

IntelliJ IDEA集成MarsCode AI

IntelliJ IDEA集成MarsCode AI IDEA中安裝插件 安裝完畢之后登錄自己的賬號 點擊鏈接&#xff0c;注冊賬號 https://www.marscode.cn/events/s/i5DRGqqo/ 可以選擇不同的模型

日期格式與字符串不匹配bug

異常特征&#xff1a;java.lang.IllegalArgumentException: invalid comparison: java.time.LocalDateTime and java.lang.String ### Error updating database. Cause: java.lang.IllegalArgumentException: invalid comparison: java.time.LocalDateTime and java.lang.Str…

C++中的無鎖編程

引言 在當今多核處理器普及的時代&#xff0c;并發編程已成為高性能應用程序開發的關鍵技術。傳統的基于鎖的同步機制雖然使用簡單&#xff0c;但往往會帶來性能瓶頸和死鎖風險。無鎖編程&#xff08;Lock-Free Programming&#xff09;作為一種先進的并發編程范式&#xff0c…

FastGPT 引申:借鑒 FastGPT 基于MySQL + ES 實現知識庫(含表結構以及核心代碼)

文章目錄 FastGPT 引申&#xff1a;借鑒 FastGPT 基于MySQL ES 實現知識庫&#xff08;含表結構以及核心代碼&#xff09;一、整體思路二、存儲結構2.1 MySQL 表結構(1) knowledge_base_dataset(2) knowledge_base_data(3) knowledge_base_index(4) ai_kb_relation 2.2 Elasti…

Python學習(十四)pandas庫入門手冊

目錄 一、安裝與導入二、核心數據結構2.1 Series 類型&#xff08;一維數組&#xff09;2.2 DataFrame 類型&#xff08;二維數組&#xff09; 三、數據讀取與寫入3.1 讀取 CSV 和 Excel 文件3.2 寫入數據 四、數據清洗與處理4.1 處理缺失值4.2 數據篩選4.3 數據排序 五、數據分…

【Python 數據結構 4.單向鏈表】

目錄 一、單向鏈表的基本概念 1.單向鏈表的概念 2.單向鏈表的元素插入 元素插入的步驟 3.單向鏈表的元素刪除 元素刪除的步驟 4.單向鏈表的元素查找 元素查找的步驟 5.單向鏈表的元素索引 元素索引的步驟 6.單向鏈表的元素修改 元素修改的步驟 二、Python中的單向鏈表 ?編輯 三…

第1章:項目概述與環境搭建

第1章&#xff1a;項目概述與環境搭建 學習目標 了解YunChangAction靈感記錄應用的整體架構和功能掌握SwiftUI開發環境的配置方法創建項目基礎結構并理解文件組織方式實現應用的啟動屏幕和基本主題設置 理論知識講解 靈感記錄應用概述 靈感記錄應用是一種專門設計用來幫助…