FFmpeg播放视频类,可复用
来源:互联网 发布:深圳平湖淘宝客服招聘 编辑:程序博客网 时间:2024/04/30 12:50
音频播放类可以点这里:音频播放类
原理介绍:视频播放类都是基于音频播放类改变过来的,加上了音频视频同步,基本做出了效果
.h头文件
#ifndef FFMPEGPLAYER_H#define FFMPEGPLAYER_H#define MAX_AUDIO_FRAME_SIZE 192000#define SDL_AUDIO_BUFFER_SIZE 1024#define MAX_AUDIO_SIZE ( 10*16 * 1024)#define MAX_VIDEO_SIZE ( 10*256 * 1024)#define FLUSH_DATA "FLUSH"extern "C"{ #include <libavcodec\avcodec.h> #include <libavformat\avformat.h> #include <libswscale\swscale.h> #include <libswresample\swresample.h> #include <include/SDL2/SDL.h> #include <include/SDL2/SDL_thread.h>}extern int VOL;#include<QThread>#include<QTimer>#include<QImage>enum PlayerStatus{playingStatus,pausingStatus,stopStatus,bufferingStatus};typedef struct PacketQueue { AVPacketList *first_pkt, *last_pkt; int nb_packets; int size; SDL_mutex *mutex; SDL_cond *cond;} PacketQueue;typedef struct{ AVFormatContext* afct; // AVPacket pkt; // ////////////////////////////common part SwrContext* swr_ctx ;// AVFrame *wanted_frame;// uint8_t* audio_pkt_data; int audio_pkt_size; // AVFrame *frame; // AVCodecContext *acct;// AVStream *audio_st; int audiostream; double audio_clock; unsigned int audio_buf_size; // unsigned int audio_buf_index; // bool isBuffering; bool seek_req; qint64 seek_pos; PacketQueue audioq; // ///////////////////// audio and video AVCodecContext *vcct; int videostream; double video_clock; PacketQueue videoq; AVStream *video_st; SDL_Thread *video_tid; //视频线程id}mediaState;class FFmpegPlayer : public QThread{ Q_OBJECTpublic: explicit FFmpegPlayer(QObject *parent = 0); void setMedia(const QString,bool isMV=false); void stop(); void pause(){SDL_PauseAudio(1);} void play(){ SDL_PauseAudio(0);} inline void updateStatus(){ if(!m_MS.acct)return;emit sig_CurrentMediaStatus(getPlayerStatus());} /*zero means pause ,one means playing*/ PlayerStatus getPlayerStatus() const; /*duration with now playing the media */ inline qint64 getDuration(){ if(!m_MS.acct)return 0;return m_MS.afct->duration;} /*get current media time value*/ inline qint64 getCurrentTime(){return m_MS.audio_clock*1000000;} QTimer *m_timer; void FreeAllocSpace();protected: virtual void run();signals: void sig_BufferingPrecent(double); void sig_CurImageChange(QImage); void sig_CurrentMediaChange(const QString&,bool isMv); void sig_CurrentMediaDurationChange(qint64); void sig_PositionChange(qint64); void sig_CurrentMediaFinished(); void sig_CurrentMediaStatus(PlayerStatus); void sig_CurrentMediaError();public slots: void slot_timerWork(); void setVol(int vol){VOL=vol;} void seek(qint64 );private: QString m_url; mediaState m_MS;};
.cpp
#include "FFmpegPlayer.h"#include<QDebug>#include<windows.h>#include<QTime>#include<QImage>static FFmpegPlayer *ffplayerPointer=NULL; //保存对象地址#define USE_MUTE 1static bool isquit=false; //清空了int VOL=80;// 包队列初始化void packet_queue_init(PacketQueue* q){ q->last_pkt = NULL; q->first_pkt = NULL;#if USE_MUTE q->mutex = SDL_CreateMutex(); q->cond = SDL_CreateCond();#endif}// 放入packet到队列中,不带头指针的队列int packet_queue_put(PacketQueue*q, AVPacket *pkt){ AVPacketList *pktl; if (av_dup_packet(pkt) < 0) return -1; pktl = (AVPacketList*)av_malloc(sizeof(AVPacketList)); if (!pktl) return -1; pktl->pkt = *pkt; pktl->next = nullptr;#if USE_MUTE SDL_LockMutex(q->mutex);#endif if (!q->last_pkt) // 队列为空,新插入元素为第一个元素 q->first_pkt = pktl; else // 插入队尾 q->last_pkt->next = pktl; q->last_pkt = pktl; q->nb_packets++; q->size += pkt->size;#if USE_MUTE SDL_CondSignal(q->cond); SDL_UnlockMutex(q->mutex);#endif return 0;}// 从队列中取出packetint packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { AVPacketList *pkt1=NULL; int ret;#if USE_MUTE SDL_LockMutex(q->mutex);#endif for (;;) { if (isquit) return -1; pkt1 = q->first_pkt; if (pkt1) { q->first_pkt = pkt1->next; if (!q->first_pkt) { q->last_pkt = NULL; } q->nb_packets--; q->size -= pkt1->pkt.size; *pkt = pkt1->pkt; av_free(pkt1); ret = 1; break; } else if (!block) { ret = 0; break; } else {#if USE_MUTE SDL_CondWait(q->cond, q->mutex);#endif } }#if USE_MUTE SDL_UnlockMutex(q->mutex);#endif return ret;}void packet_queue_flush(PacketQueue *q){#if USE_MUTE SDL_LockMutex(q->mutex);#endif AVPacketList *pkt=NULL, *pkt1=NULL; for(pkt = q->first_pkt; pkt != NULL; pkt = pkt1) { pkt1 = pkt->next; if(pkt1->pkt.data != (uint8_t *)"FLUSH") { } av_free_packet(&pkt->pkt); av_freep(&pkt); } q->last_pkt = NULL; q->first_pkt = NULL; q->nb_packets = 0; q->size = 0;#if USE_MUTE SDL_UnlockMutex(q->mutex);#endif}//////////////////////////////////////////////解码音频数据int audio_decode_frame(mediaState* MS, uint8_t* audio_buf, int buf_size){ int len1; int data_size = 0; if (isquit) return -1; while (true) { while (MS->audio_pkt_size > 0) { int got_frame = 0; len1 = avcodec_decode_audio4(MS->acct, MS->frame, &got_frame, &MS->pkt); if (len1 < 0) // 出错,跳过 { MS->audio_pkt_size = 0; break; } MS->audio_pkt_data += len1; MS->audio_pkt_size -= len1; data_size = 0; if (got_frame) data_size = av_samples_get_buffer_size(nullptr, MS->acct->channels,MS-> frame->nb_samples, MS->acct->sample_fmt, 1); if (MS->frame->channels > 0 && MS->frame->channel_layout == 0) MS->frame->channel_layout = av_get_default_channel_layout(MS->frame->channels); else if (MS->frame->channels == 0 && MS->frame->channel_layout > 0) MS->frame->channels = av_get_channel_layout_nb_channels(MS->frame->channel_layout); if (MS->swr_ctx) { swr_free(&MS->swr_ctx); MS->swr_ctx = nullptr; } MS->swr_ctx = swr_alloc_set_opts(nullptr, MS->wanted_frame->channel_layout, (AVSampleFormat)MS->wanted_frame->format, MS->wanted_frame->sample_rate, MS->frame->channel_layout, (AVSampleFormat)MS->frame->format, MS->frame->sample_rate, 0, nullptr); if (!MS->swr_ctx || swr_init(MS->swr_ctx) < 0) { qDebug() << "swr_init failed:" << endl; break; } int dst_nb_samples = av_rescale_rnd(swr_get_delay(MS->swr_ctx, MS->frame->sample_rate) + MS->frame->nb_samples, MS->frame->sample_rate, MS->frame->sample_rate, AVRounding(1)); int len2 = swr_convert(MS->swr_ctx, &audio_buf, dst_nb_samples,(const uint8_t**)MS->frame->data, MS->frame->nb_samples);//这个才是最重要的~前面所做的工作都是为这个 if (len2 < 0) { qDebug() << "swr_convert failed\n"; break; }//[][]相当重要的一步,转换成时间 int resampled_data_size = len2 * MS->wanted_frame->channels* av_get_bytes_per_sample((AVSampleFormat)MS->wanted_frame->format); int n = 2 * MS->audio_st->codec->channels; MS->audio_clock += (double)resampled_data_size/(double)(n * MS->audio_st->codec->sample_rate);//[][] return MS->wanted_frame->channels * len2 * av_get_bytes_per_sample((AVSampleFormat)MS->wanted_frame->format); } //end while if (MS->pkt.buf) av_free_packet(&MS->pkt); //删除包 if (packet_queue_get(&MS->audioq,&MS->pkt,0)<=0) //重新从队列中获取包 { return -1; } //收到这个数据 说明刚刚执行过跳转 现在需要把解码器的数据 清除一下 if(strcmp((char*)MS->pkt.data,FLUSH_DATA) == 0) { avcodec_flush_buffers(MS->audio_st->codec); av_free_packet(&MS->pkt); continue; } if (MS->pkt.pts != AV_NOPTS_VALUE) { MS->audio_clock = (double)av_q2d(MS->audio_st->time_base) * (double)MS->pkt.pts; } MS->audio_pkt_data =MS->pkt.data; MS->audio_pkt_size = MS->pkt.size; }}// 解码后的回调函数void audio_callback(void* userdata, Uint8* stream, int len){ mediaState* MS = (mediaState*)userdata; int len1, audio_size; SDL_memset(stream, 0, len); if (isquit) return; while (len > 0) { uint8_t audio_buff[MAX_AUDIO_FRAME_SIZE*2]; if (MS->audio_buf_index >= MS->audio_buf_size) { audio_size = audio_decode_frame(MS, audio_buff, sizeof(audio_buff)); if (isquit) return; if (audio_size < 0) { MS->audio_buf_size = 1024; SDL_memset(audio_buff, 0, MS->audio_buf_size); } else MS->audio_buf_size = audio_size; MS->audio_buf_index = 0; } len1 = MS->audio_buf_size - MS->audio_buf_index; if (len1 > len) len1 = len; SDL_MixAudio(stream, audio_buff + MS->audio_buf_index, len, VOL); len -= len1; stream += len1; MS->audio_buf_index += len1; }}static double synchronize_video(mediaState *MS, AVFrame *src_frame, double pts)//用于音视频同步 { double frame_delay; if (pts != 0) { /* if we have pts, set video clock to it */ MS->video_clock = pts; } else { /* if we aren't given a pts, set it to the clock */ pts = MS->video_clock; } /* update the video clock */ frame_delay = av_q2d(MS->video_st->codec->time_base); /* if we are repeating a frame, adjust clock accordingly */ frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); MS->video_clock += frame_delay; return pts;}int video_thread(void *arg){ mediaState *is = (mediaState *) arg; AVPacket pkt1, *packet = &pkt1; int ret, got_picture, numBytes; double video_pts = 0; //当前视频的pts double audio_pts = 0; //音频pts ///解码视频相关 AVFrame *pFrame, *pFrameRGB; uint8_t *out_buffer_rgb; //解码后的rgb数据 struct SwsContext *img_convert_ctx; //用于解码后的视频格式转换 AVCodecContext *pCodecCtx = is->video_st->codec; //视频解码器 pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); ///这里我们改成了 将解码后的YUV数据转换成RGB32 img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height); out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); avpicture_fill((AVPicture *) pFrameRGB, out_buffer_rgb, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height); while(1) { if (isquit) { break; } if (SDL_AUDIO_PAUSED == SDL_GetAudioStatus()) //判断暂停 { SDL_Delay(1); continue; } if (packet_queue_get(&is->videoq, packet, 0) <= 0) //非block { SDL_Delay(1); //队列只是暂时没有数据而已 continue; } //收到这个数据 说明刚刚执行过跳转 现在需要把解码器的数据 清除一下 if(strcmp((char*)packet->data,FLUSH_DATA) == 0) { avcodec_flush_buffers(is->video_st->codec); av_free_packet(packet); continue; } ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); if (ret < 0) { qDebug()<<"decode error.\n"; av_free_packet(packet); continue; }//音视频同步 if (packet->dts == AV_NOPTS_VALUE && pFrame->opaque&& *(uint64_t*) pFrame->opaque != AV_NOPTS_VALUE) { video_pts = *(uint64_t *) pFrame->opaque; } else if (packet->dts != AV_NOPTS_VALUE) { video_pts = packet->dts; } else { video_pts = 0; } video_pts *= av_q2d(is->video_st->time_base); video_pts = synchronize_video(is, pFrame, video_pts); /* if (is->seek_flag_video) { //发生了跳转 则跳过关键帧到目的时间的这几帧 if (video_pts < is->seek_time) { av_free_packet(packet); continue; } else { is->seek_flag_video = 0; } }*/ while(1) { if (isquit) { break; } audio_pts = is->audio_clock; //主要是 跳转的时候 我们把video_clock设置成0了 //因此这里需要更新video_pts //否则当从后面跳转到前面的时候 会卡在这里 video_pts = is->video_clock; if (video_pts <= audio_pts) break; int delayTime = (video_pts - audio_pts) * 1000; delayTime = delayTime > 5 ? 5:delayTime; SDL_Delay(delayTime); }//同步结束 if (got_picture) { sws_scale(img_convert_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); //把这个RGB数据 用QImage加载 QImage tmpImg((uchar *)out_buffer_rgb,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32); QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示 emit ffplayerPointer->sig_CurImageChange(image); //调用激发信号的函数 } av_free_packet(packet); } av_free(pFrame); av_free(pFrameRGB); av_free(out_buffer_rgb); emit ffplayerPointer->sig_CurImageChange(QImage()); //刷新下MV背景}int interrupt_cb(void *ctx)//网络不畅就会一直做这里 ,正在播放也会call这里但频率不如网络不畅高{ mediaState *MS=(mediaState*)ctx; return 0;}FFmpegPlayer::FFmpegPlayer(QObject *parent) : QThread(parent){ ffplayerPointer=this; m_timer=new QTimer; connect(m_timer,SIGNAL(timeout()),this,SLOT(slot_timerWork())); m_timer->start(30); av_register_all(); avformat_network_init(); #ifndef Q_OS_WIN32 CoInitializeEx(NULL, COINIT_MULTITHREADED);//防止有些windows64找不到audio设备 #endif packet_queue_init(&m_MS.audioq); packet_queue_init(&m_MS.videoq); m_MS={0};//自动将能初始化为0的都初始化为0}void FFmpegPlayer::setMedia(const QString url, bool isMV){ stop(); emit sig_CurrentMediaChange(url,isMV); m_url=url; start(); setPriority(QThread::HighestPriority);}void FFmpegPlayer::stop(){ isquit=1; m_url=""; Sleep(200);//等待退出线程}PlayerStatus FFmpegPlayer::getPlayerStatus() const{ if(m_MS.isBuffering) return PlayerStatus::bufferingStatus; if(SDL_AUDIO_PLAYING ==SDL_GetAudioStatus()) return PlayerStatus::playingStatus; return PlayerStatus::pausingStatus;}void FFmpegPlayer::FreeAllocSpace() //存在内在{ SDL_CloseAudio();//Close SDL SDL_Quit(); if(m_MS.wanted_frame) //avframe freee { av_frame_free(&m_MS.wanted_frame); } if(m_MS.frame) //avframe freee { av_frame_free(&m_MS.frame); } if(m_MS.afct) //format context { avformat_close_input(&m_MS.afct); avformat_free_context(m_MS.afct); } if(m_MS.acct)//audio context { avcodec_close(m_MS.acct); avcodec_free_context(&m_MS.acct); } if(m_MS.vcct)//video context { avcodec_close(m_MS.vcct); avcodec_free_context(&m_MS.vcct); } if(m_MS.swr_ctx) //重采样 freee { swr_free(&m_MS.swr_ctx); } if(m_MS.audio_pkt_data)//buff free { av_freep(m_MS.audio_pkt_data); } packet_queue_flush(&m_MS.audioq);//队列freee packet_queue_flush(&m_MS.videoq);//队列freee m_MS={0};//自动将能初始化为0的都初始化为NULL}void FFmpegPlayer::slot_timerWork(){ if(m_MS.frame&&!m_MS.isBuffering) emit sig_PositionChange(getCurrentTime()); updateStatus();}void FFmpegPlayer::seek(qint64 pos){ if(!m_MS.seek_req) { m_MS.seek_pos=pos; m_MS.seek_req=true; }}void FFmpegPlayer::run(){ isquit=0; SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER); // 读取文件头,将格式相关信息存放在AVFormatContext结构体中 if (avformat_open_input(&m_MS.afct, m_url.toUtf8().data(), nullptr, nullptr) != 0) { FreeAllocSpace(); return; // 打开失败 } m_MS.afct->interrupt_callback.callback = interrupt_cb;//--------注册回调函数 m_MS.afct->interrupt_callback.opaque = &m_MS; // 检测文件的流信息 if (avformat_find_stream_info(m_MS.afct, nullptr) < 0) { FreeAllocSpace(); return; // 没有检测到流信息 stream infomation } //查找第一个视频流 video stream m_MS.audiostream = -1; m_MS.videostream = -1; for (unsigned int i = 0; i < m_MS.afct->nb_streams; i++) { if (m_MS.afct->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { m_MS.videostream = i; } if (m_MS.afct->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && m_MS.audiostream < 0) { m_MS.audiostream = i; } } // 3. 根据读取到的流信息查找相应的解码器并打开 if (m_MS.audiostream == -1&&m_MS.videostream==-1) { FreeAllocSpace(); return; // 没有检测到流信息 stream infomation }//[0][1] for audio if(m_MS.audiostream!=-1) { m_MS.acct = m_MS.afct->streams[m_MS.audiostream]->codec; // codec context AVCodec* acodec = avcodec_find_decoder(m_MS.acct->codec_id); if (!acodec) { qDebug() << "Unsupported codec!" << endl; FreeAllocSpace(); return; } m_MS.audio_st=m_MS.afct->streams[m_MS.audiostream]; avcodec_open2(m_MS.acct, acodec, nullptr); //open }//[2][3]for video if(m_MS.videostream!=-1) { m_MS.vcct = m_MS.afct->streams[m_MS.videostream]->codec; // codec context AVCodec* vcodec = avcodec_find_decoder(m_MS.vcct->codec_id); if (!vcodec) { qDebug() << "Unsupported codec!" << endl; FreeAllocSpace(); return; } m_MS.video_st=m_MS.afct->streams[m_MS.videostream]; avcodec_open2(m_MS.vcct, vcodec, nullptr); //open }//[][] // Set audio settings from codec info SDL_AudioSpec wanted_spec, spec; wanted_spec.freq = m_MS.acct->sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.channels = m_MS.acct->channels; wanted_spec.silence = 0; wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = audio_callback; wanted_spec.userdata = &m_MS; if (SDL_OpenAudio(&wanted_spec, &spec) < 0) { qDebug() << "Open audio failed:" << SDL_GetError() << endl; FreeAllocSpace(); return ; } m_MS.wanted_frame=av_frame_alloc(); m_MS.frame=av_frame_alloc(); m_MS.wanted_frame->format = AV_SAMPLE_FMT_S16; m_MS.wanted_frame->sample_rate = spec.freq; m_MS.wanted_frame->channel_layout = av_get_default_channel_layout(spec.channels); m_MS.wanted_frame->channels = spec.channels; if(m_MS.videostream!=-1) m_MS.video_tid = SDL_CreateThread(video_thread, "video_thread", &m_MS); SDL_PauseAudio(0); int get=0; AVPacket packet; while (true) //这里有一个顺序!先判断退出线程信号~再 读 再写入 { SDL_Delay(1); if (isquit) { wanted_spec.callback=NULL; wanted_spec.userdata=NULL; break; } if(get<0&&!m_MS.audioq.first_pkt)//end of the file 队列不为空!而且完成@! { wanted_spec.callback=NULL; wanted_spec.userdata=NULL; break; } //seek part if (m_MS.seek_req) { int stream_index = -1; if (m_MS.videostream >= 0) stream_index = m_MS.videostream; else if (m_MS.audiostream >= 0) stream_index = m_MS.audiostream; AVRational aVRational = {1, AV_TIME_BASE}; if (stream_index >= 0) { m_MS.seek_pos = av_rescale_q(m_MS.seek_pos, aVRational, m_MS.afct->streams[stream_index]->time_base); } if (av_seek_frame(m_MS.afct, stream_index, m_MS.seek_pos, AVSEEK_FLAG_BACKWARD) < 0) { fprintf(stderr, "%s: error while seeking\n",m_MS.afct->filename); } else { AVPacket packet; //分配一个packet av_new_packet(&packet, 10); strcpy((char*)packet.data,FLUSH_DATA); if (m_MS.audiostream >= 0) //audio { packet_queue_flush(&m_MS.audioq); //清除队列 packet_queue_put(&m_MS.audioq, &packet); //往队列中存入用来清除的包 } if (m_MS.videostream >= 0) { packet_queue_flush(&m_MS.videoq); //清除队列 packet_queue_put(&m_MS.videoq, &packet); //往队列中存入用来清除的包 m_MS.video_clock = 0; } } m_MS.seek_req = 0; } if (m_MS.audioq.size > MAX_AUDIO_SIZE || m_MS.videoq.size > MAX_VIDEO_SIZE)//防止一下子把音频全部读完了~ continue; get= av_read_frame(m_MS.afct, &packet); //read frame if(get==0)//=0就是正确的~再添加进队列 { if(packet.stream_index == m_MS.videostream) packet_queue_put(&m_MS.videoq,&packet); else if (packet.stream_index == m_MS.audiostream) packet_queue_put(&m_MS.audioq, &packet); else av_free_packet(&packet); m_MS.isBuffering=false; //显示界面显示有无缓冲 } } if(!isquit) //It finished automatically when played to end of the media emit sig_CurrentMediaFinished(); isquit=1; FreeAllocSpace();}
更多文章:http://blog.csdn.net/what951006?viewmode=list
powered by:小乌龟在大乌龟背上
0 0
- FFmpeg播放视频类,可复用
- FFMpeg视频播放器
- ffmpeg播放视频音频
- ffmpeg视频播放过程
- ffmpeg用来播放视频
- ffmpeg SDL播放视频
- FFmpeg视频播放流程
- FFmpeg视频播放-SurfaceView
- ffmpeg 视频播放
- FFMPEG 之视频播放
- 自学FFmpeg播放视频
- MFC 播放视频 FFMPEG SDL
- sdl+ffmpeg视频播放器
- FFmpeg+SDL视频播放(1)
- FFmpeg+SDL视频播放(2)
- FFmpeg+SDL视频播放(4)
- FFmpeg+SDL视频播放(3)
- ffmpeg + sdl 视频播放器
- .NET学习笔记(3)——深入.net平台和C#编程
- 网络流24题4 魔术球问题 ssl 2604 code[vs] 1234
- 深入理解Activity启动流程(三)–Activity启动的详细流程1
- scala 学习(一)——for循环
- Java实现Html转PDF
- FFmpeg播放视频类,可复用
- UVa10237 Bishops
- HData——ETL 数据导入/导出工具
- window下设置多个用户远程连接
- 文件的复制
- BindingException: Invalid bound statement (not found) 错误解决办法
- 【Unity&Shader】遮罩效果黑暗
- 代码规范(草案)
- bzoj 1874: [BeiJing2009 WinterCamp]取石子游戏 Nim游戏+SG函数