linux下播放器的設計和開發

本文根據DawnLightPlayer的開發經驗寫成。DawnLithtPlayer是今天3月份開始,和maddrone一塊兒在業餘時間開發的一個跨平臺,多線程的播放器,主要是在Linux下面開發的,文中所用示例代碼均截自其中。
DawnLightPlayer目前能夠運行在Linux和Windows系統上,並使用VC和Python開發了GUI,支持大部分的音視頻文件格式和網絡流,另外新增對CMMB協議的支持,不支持 RMVB, SWF 等還沒有公開協議的視頻文件格式。

目錄:
一. 播放器的流程
   1. 輸入
   2. 解碼
   3. 輸出
二. 播放器的實現
   1. 輸入實現
   2. 解碼線程實現
   3. 輸出線程實現
三. 視頻輸出庫
   1. SDL (多平臺,支持硬件縮放)
   2. DirectX DirectDraw (win32平臺,支持硬件縮放)
   3. OpenGL (多平臺,支持硬件縮放)
   4. X11 (Linux/Unix)
   5. FrameBuffer (Linux, 無硬件縮放)
四. 音頻輸出
   1. OSS (Open Sound System for Linux)
   2. ALSA (Advanced Linux Sound Architecture)
   3. DirectSound (WIN32)
五. 音視頻同步
   1. 以音頻爲基準同步視頻
   2. 以視頻爲基準同步音頻
   3. 同步於一個外部時鐘
六. 截圖
   1. 使用jpeglib保存成jpeg文件
   2. 使用libpng保存成png文件
七. YUV RGB 軟件轉換
八. 軟件縮放



一. 播放器的流程

1. 輸入 : 從文件或網絡等讀取原數據,如 x.avi, x.mov, rtsp://xxx, 對原數據進行解析,好比文件,首先要分析文件格式,從文件中取得音視頻編碼參數,視頻時間長度等信息,而後要從其中取出音頻編碼數據和視頻編碼數據送到解 碼部分,這裏暫稱這種編碼源數據塊爲 packet。

2. 解碼 : 初始化時,利用輸入端從源數據中取得的信息調用不一樣的解碼庫初始化;而後接收輸入端傳送來的音視頻編碼數據,分別進行音頻解碼和視頻解碼,視頻解碼出來的 數據通常是 YUV 或 RGB 數據,這裏暫稱爲 picture, 音頻解碼出來的數據是採樣數據,是聲卡能夠播放的數據,這裏暫稱爲 sample。 解碼所得的數據接下來送到輸出部分。

3. 輸出 : 接收解碼部分送來的 picture 和 sample 並顯示。 視頻顯示通常使用某個圖形庫,如 SDL, Xlib, DirectDraw, OpengGL, FrameBuffer等, 音頻輸出是把 sample 寫入系統的音頻驅動,由音頻驅動送入聲卡播放, 可用的音頻輸出有 ALSA, OSS, SDL, DirectSound, WaveOut等。

二. 播放器的實現

推薦實現方案
一個audio_packet隊列,一個video_packet隊列,一個picture隊列,一個sample隊列
一個input線程,兩個decode線程,兩個output線程,一個UI控制線程

1. 輸入實現
對 文件的解析,首先要了解文件的格式,文件格式通常稱爲文件容器。公開的文件格式,按格式協議讀取分析就能夠了,但像RMVB,SWF這種目前還不公開格式 的文件,就很差辦,也是目前通常播放器的困難。通常的文件格式的解析libavformat庫已經作了,只要使用它就行,下面給出示例代碼段:

初始化:
static int avin_file_init(void)
{
    AVFormatParameters params, *ap = ¶ms;
    err = av_open_input_file( &fmtctx, input_filename, NULL, 0, ap );
    if ( err < 0 )
    {
        av_log(NULL, AV_LOG_ERROR, "%d: init input from file error\n", __LINE__);
        print_error( input_filename, err );
        return -1;
    }

    fmtctx->flags |= AVFMT_FLAG_GENPTS;

    err = av_find_stream_info( fmtctx );
    if ( err < 0 )
    {
        av_log(NULL, AV_LOG_ERROR, "%d: init input from file error\n", __LINE__);
        print_error( input_filename, err );
        return -1;
    }

    if (fmtctx->pb) fmtctx->pb->eof_reached = 0;
    dump_format( fmtctx, 0, input_filename, 0 );

    ....
}
讀取packet:
while( 1 )
{
    AVPacket *pkt = NULL;
    pkt = av_malloc( sizeof(AVPacket) );
    ret = av_read_frame(fmtctx, pkt);
    
送出packet到解碼部分:
    能夠memcpy, 或用LinkList結構處理,如:
    push_to_video_packet_queue(pkt);
}

若是是本身的私有輸入,好比移動電視的視頻輸入,代碼以下,部分是僞代碼:
while( 1 )
{
    your_parse_code();
    size = your_get_video_data(buf);

    pkt = av_mallocz( sizeof(AVPacket) );
    x = av_new_packet( pkt, vret);
    memcpy( pkt->data, buf, size );
    pkt->pts = your_time;

    push_to_video_packet_queue(pkt);
}

2. 解碼線程實現
解碼是個算法大課題,大多隻能使用已有的解碼庫,如libavcodec,下面示例代碼:
while ( 1 )
{
    AVPicture *picture;
    AVPacket *pkt = pop_from_video_packet_queue();
    AVFrame *frame = avcodec_alloc_frame();
    avcodec_decode_video(video_ctxp, frame, &got_picture, pkt->data, pkt->size);
    if ( got_picture )
    {
        convert_frame_to_picture( picture, frame );
        picture->pts = pkt->pts;
        push_to_picture_queue( picture );
    }
}
音頻雷同

3. 輸出線程實現

視 頻輸出要控制FPS,好比25幀每秒的視頻,那麼每一幀的顯示時間要是1/25秒,但把一幀RGB數據寫入顯存用不了1/25秒的時間,那麼就要控制,不 能讓25幀的數據在0.1或0.2秒的時間內就顯示完了,最簡單的實現是在每顯示一幀數據後,sleep( 1/fps - 顯示用去的時間 )。

音 視頻同步這個重要的工做也要在輸出線程裏完成。以音頻爲基準同步視頻,以視頻爲基準同步音頻,或與一個外部時鐘同步,都是可行的方法,但以音頻爲基準同步 視頻是最簡單也最有效的方法。音頻驅動只要設置好sample rate, sample size 和 channels 後, write 數據就會以此恆定的速度播放, 若是驅動的輸出 buffer 滿,則 write 就能夠等待。

視頻:
while( 1 )
{
    picture = pop_from_picture_queue();
    picture_shot( picture ); /* 截圖 */
    vo->display( picture );
    video_pts = picture->pts;
    sync_with_audio(); /* 同步 */
    control_fps(); /* FPS */
}
音頻:
while( 1 )
{
    sample = pop_from_sample_queue();
    ao->play( sample );
    now_pts = sample->pts;
}

三. 視頻輸出庫

1. SDL (多平臺,支持硬件縮放)

SDL(Simple DirectMedia Layer) is a cross-platform multimedia library designed to provide low level access to audio, keyboard, mouse, joystick, 3D hardware via OpenGL, and 2D video framebuffer.

其實SDL就是一箇中間件,它封裝了下層的OpenGL, FrameBuffer, X11, DirectX等給上層提供一個統一的API接口,使用SDL的優勢是咱們沒必要再爲X11或DirectX分別作個視頻輸出程序了。

SDL能夠直接顯示YUV數據和RGB數據,通常解碼獲得的picture都是YUV420P格式的,不用作YUV2RGB的轉換就能夠直接顯示,主要代碼以下:

static int vo_sdl_init(void)
{
    ....
    screen = SDL_SetVideoMode(ww, wh, 0, flags);
    overlay = SDL_CreateYUVOverlay(dw, dh, SDL_YV12_OVERLAY, screen);
   ....
}

static void vo_sdl_display(AVPicture *pict)
{
    SDL_Rect rect;
    AVPicture p;

    SDL_LockYUVOverlay(overlay);
    p.data[0] = overlay->pixels[0];
    p.data[1] = overlay->pixels[2];
    p.data[2] = overlay->pixels[1];
    p.linesize[0] = overlay->pitches[0];
    p.linesize[1] = overlay->pitches[2];
    p.linesize[2] = overlay->pitches[1];
    vo_sdl_sws( &p, pict ); /* only do memcpy */
    SDL_UnlockYUVOverlay(overlay);

    rect.x = dx;
    rect.y = dy;
    rect.w = dw;
    rect.h = dh;
    SDL_DisplayYUVOverlay(overlay, &rect);
}

2. DirectX DirectDraw (win32平臺,支持硬件縮放)

DirectX是window上使用較多的一種輸出,也支持直接YUV或RGB顯示,示例代碼:

static int vo_dx_init(void)
{
    DxCreateWindow();
    DxInitDirectDraw();
    DxCreatePrimarySurface();
    DxCreateOverlay();
    DetectImgFormat();
}

static void vo_dx_display(AVPicture *pic)
{
    vfmt2rgb(my_pic, pic);
    memcpy( g_image, my_pic->data[0], my_pic->linesize[0] * height );
    flip_page();
}

3. OpenGL (多平臺,支持硬件縮放)

OpenGL是3D遊戲庫,跨平臺,效率高,支持大多數的顯示加速,顯示2D RGB數據只要使用glDrawPixels函數就足夠了,同時禁用一些OpenGL管線操做效率更高,如:

    glDisable( GL_SCISSOR_TEST );
    glDisable( GL_ALPHA_TEST );
    glDisable( GL_DEPTH_TEST );
    glDisable( GL_DITHER );

4. X11 (Linux/Unix)

X11 是Unix/Linux系統平臺上的基本圖形界面庫,像普通的GTK,QT等主要都是創建在X11的基礎之上。但X11的API接口太多,複雜,很不利於 開發,基本的GUI程序通常都會使用GTK,QT等,不會直接調用X11的API,這裏只是爲了效率。MPlyaer的libvo裏有X11的完整使用代 碼,包括全屏等功能。

static void vo_x11_display(AVPicture* pic)
{
    vfmt2rgb( my_pic, pic );
    Ximg->data = my_pic->data[0];
    XPutImage(Xdisplay, Xvowin, Xvogc, Ximg,
              0, 0, 0, 0, dw, dh);
    XSync(Xdisplay, False);
    XSync(Xdisplay, False);
}

5. FrameBuffer (Linux, 無硬件縮放)

FrameBuffer是Linux內核的一部分,提供一個到顯存的存取地址的map,但沒有任何加速使用。

static void vo_fb_display(AVPicture *pic)
{
    int i;
    uint8_t *src, *dst = fbctxp->mem;

    vfmt2rgb( my_pic, pic );
    src = my_pic->data[0];

    for ( i = 0; i < fbctxp->dh; i++ )
    {
        memcpy( dst, src, fbctxp->dw * (fbctxp->varinfo.bits_per_pixel / 8) );
        dst += fbctxp->fixinfo.line_length;
        src += my_pic->linesize[0];
    }
}

四. 音頻輸出

1. OSS (Open Sound System for Linux)

OSS是Linux下面最簡單的音頻輸出了,直接write就能夠。

static int ao_oss_init(void)
{
    int i;
    dsp = open(dsp_dev, O_WRONLY);
    if ( dsp < 0 )
    {
        av_log(NULL, AV_LOG_ERROR, "open oss: %s\n", strerror(errno));
        return -1;
    }
    i = sample_rate;
    ioctl (dsp, SNDCTL_DSP_SPEED, &i);
    i = format2oss(sample_fmt);
    ioctl(dsp, SNDCTL_DSP_SETFMT, &i);
    i = channels;
    if ( i > 2 ) i = 2;
    ioctl(dsp, SNDCTL_DSP_CHANNELS, &i);

    return 0;
}

static void ao_oss_play(AVSample *s)
{
    write(dsp, s->data, s->size);
}

2. ALSA (Advanced Linux Sound Architecture)

ALSA作的比較失敗,長長的函數名。

static void ao_alsa_play(AVSample *s)
{
    int num_frames = s->size / bytes_per_sample;
    snd_pcm_sframes_t res = 0;
    uint8_t *data = s->data;

    if (!alsa_handle)
        return ;

    if (num_frames == 0)
        return ;

rewrite:
    res = snd_pcm_writei(alsa_handle, data, num_frames);
    if ( res == -EINTR )
        goto rewrite;
    if ( res < 0 )
    {
        snd_pcm_prepare(alsa_handle);
        goto rewrite;
    }
    if ( res < num_frames )
    {
        data += res * bytes_per_sample;
        num_frames -= res;
        goto rewrite;
    }
}

3. DirectSound (WIN32)

MS DirectX的一部分,它的缺點是不如Linux裏面的OSS或ALSA那樣,在沒有sample寫入的時候,自動 silent,DirectSound在播放過程當中,當沒有sample數據送入輸出線程時,它老是回放最後0.2或0.5秒的數據。因爲只是最近移植 DawnLightPlayer才使用起Windows,不太瞭解其機制。

static void dsound_play(AVSample *s)
{
    int wlen, ret, len = s->size;
    uint8_t *data = s->data;

    while ( len > 0 )
    {
        wlen = dsound_getspace();
        if ( wlen > len ) wlen = len;
        ret = write_buffer(data, wlen);
        data += ret;
        len -= ret;
        usleep(10*1000);
    }
}

五. 音視頻同步

1. 以音頻爲基準同步視頻

視頻輸出線程中以下處理:
    start_time = now();
    ....
    vo->display( picture );
    last_video_pts = picture->pts;
    end_time = now();
    rest_time = end_time - start_time;
    av_diff = last_audio_pts - last_video_pts;
    if ( av_diff > 0.2 )
    {
        if ( av_diff < 0.5 ) rest_time -= rest_time / 4;
        else rest_time -= rest_time / 2;
    }
    else if ( av_diff < -0.2)
    {
        if ( av_diff > -0.5 ) rest_time += rest_time / 4;
        else rest_time += rest_time / 2;
    }
    if ( rest_time > 0 )
        usleep(rest_time);

2. 以視頻爲基準同步音頻


3. 同步於一個外部時鐘



六. 截圖

截圖能夠在解碼線程作,也能夠在輸出線程作,見前面的輸出線程部分。只要在display前把picture保存起來便可。通常加一些編碼,如保存成 PNG 或 JPEG 格式。

1. 使用jpeglib保存成jpeg文件

static void draw_jpeg(AVPicture *pic)
{
    char fname[128];
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    JSAMPROW row_pointer[1];
    int row_stride;
    uint8_t *buffer;

    if ( !po_status )
        return ;

    vfmt2rgb24(my_pic, pic);
    buffer = my_pic->data[0];

#ifdef __MINGW32__
    sprintf(fname, "%s\\DLPShot-%d.jpg", get_save_path(), framenum++);
#else
    sprintf(fname, "%s/DLPShot-%d.jpg", get_save_path(), framenum++);
#endif
    fp = fopen (fname, "wb");
    if (fp == NULL)
    {
        av_log(NULL, AV_LOG_ERROR, "fopen %s error\n", fname);
        return;
    }
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    jpeg_stdio_dest(&cinfo, fp);

    cinfo.image_width = width;
    cinfo.image_height = height;
    cinfo.input_components = 3;
    cinfo.in_color_space = JCS_RGB;

    jpeg_set_defaults(&cinfo);
    cinfo.write_JFIF_header = TRUE;
    cinfo.JFIF_major_version = 1;
    cinfo.JFIF_minor_version = 2;
    cinfo.density_unit = 1;
    cinfo.X_density = jpeg_dpi * width / width;
    cinfo.Y_density = jpeg_dpi * height / height;
    cinfo.write_Adobe_marker = TRUE;

    jpeg_set_quality(&cinfo, jpeg_quality, jpeg_baseline);
    cinfo.optimize_coding = jpeg_optimize;
    cinfo.smoothing_factor = jpeg_smooth;
    if ( jpeg_progressive_mode )
    {
        jpeg_simple_progression(&cinfo);
    }
    jpeg_start_compress(&cinfo, TRUE);

    row_stride = width * 3;
    while (cinfo.next_scanline < height)
    {
        row_pointer[0] = &buffer[cinfo.next_scanline * row_stride];
        (void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }

    jpeg_finish_compress(&cinfo);
    fclose(fp);
    jpeg_destroy_compress(&cinfo);

    return ;
}

2. 使用libpng保存成png文件

static void draw_png(AVPicture *pic)
{
    int k;
    png_byte *row_pointers[height]; /* GCC C99 */

    if ( init_png() < 0 )
    {
        av_log(NULL, AV_LOG_ERROR, "draw_png: init png error\n");
        return ;
    }

    vfmt2rgb24( my_pic, pic );

    for ( k = 0; k < height; k++ )
        row_pointers[k] = my_pic->data[0] + my_pic->linesize[0] * k;

    png_write_image(png.png_ptr, row_pointers);

    destroy_png();
}


七. YUV RGB 轉換

YUV 與RGB的轉換和縮放,通常在低端設備上,要有硬件加速來作,不然CPU吃不消。在現在的高端PC上,可使用軟件來作,libswscale庫正爲此而 來。libswscale針對X86 CPU已經作了優化,如使用 MMX, SSE, 3DNOW 等 CPU 相關的多媒體指令。

static int vfmt2rgb(AVPicture *dst, AVPicture *src)
{
    static struct SwsContext *img_convert_ctx;

    img_convert_ctx = sws_getCachedContext(img_convert_ctx,
                      width, height, src_pic_fmt,
                      width, height, my_pic_fmt, SWS_X, NULL, NULL, NULL);

    sws_scale(img_convert_ctx, src->data, src->linesize,
              0, width, dst->data, dst->linesize);

    return 0;
}

好比從 YUV420P 到 RGB24 的轉換,只要設置

src_pic_fmt = PIX_FMT_YUV420P ;
my_pic_fmt = PIX_FMT_RGB24 ;


八. 軟件縮放

軟件縮放就可使用上述的 libswscale 庫,調用代碼基本同樣,只是改一下目標picture的width和height,如放大兩倍:

static int zoom_2(AVPicture *dst, AVPicture *src)
{
    static struct SwsContext *img_convert_ctx;

    img_convert_ctx = sws_getCachedContext(img_convert_ctx,
                      width, height, src_pic_fmt,
                      width*2, height*2, my_pic_fmt, SWS_X, NULL, NULL, NULL);

    sws_scale(img_convert_ctx, src->data, src->linesize,
              0, width*2, dst->data, dst->linesize);

    return 0;
}算法

相關文章
相關標籤/搜索