Android视音频开发初探【三】(简单的播放器)

上一篇 Android视音频开发初探【二】(简单的相机推流器)

demo地址https://github.com/ColorfulHorse/learnFFmpeg

简单流程

上一篇博客中已经完成了一个简单的推流器,这篇我们来实现一个简单的播放器来拉流播放;实际上就是把之前推流器的流程倒过来走了一遍:

  1. 连接流媒体服务器取流
  2. 码流解码得到原数据
  3. 将得到的yuv数据转换为rgb数据进行绘制显示

JNI绑定

java 部分

class LyjPlayer {

    companion object {
        // 一些状态常量,用于回调
        const val STATE_CONNECTED = 0
        const val STATE_START = STATE_CONNECTED + 1
        const val STATE_STOP = STATE_START + 1

        const val ERROR_CONNECT_TIMEOUT = 0
        const val ERROR_STREAM = ERROR_CONNECT_TIMEOUT + 1
        const val NONE_VIDEO_STREAM = ERROR_STREAM + 1
        const val UNKNOW = NONE_VIDEO_STREAM + 1

        init {
            LibLoader.loadLib("lyjplayer")
        }
    }

    init {
        initPlayer()
    }

    external fun initPlayer()
    // 设置surface用于显示画面
    external fun setSurface(surface: Surface)

    external fun setVideoCallBack(callback: VideoCallBack)

    external fun startPlay(url: String): Int

    external fun stopPlay(): Int

    external fun release()
}

native 部分

这里用一个map用于保存多个播放器实例,根据java层对象toString方法得到的字符串作为key标识native层的对象,做到一一对应。

另外在native层中需要通过ANativeWindow(可以认为是native层的surface)来显示画面,所以在player_set_surface方法中根据java层传入surface创建对应实例。

register_jni.cpp

template<class T>
int arrayLen(T &array) {
    return (sizeof(array) / sizeof(array[0]));
}

#ifdef __cplusplus
extern "C" {
#endif

const char *cls_player = "com/lyj/learnffmpeg/LyjPlayer";

// 实例集合
map<string, LyjPlayer *> player_map;

// 根据java object生成key标识native层对象,这里的key是java bean的内存地址字符串
const string getKey(JNIEnv *env, jobject obj) {
    jclass cls = env->GetObjectClass(obj);
    jmethodID mid = env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
    jstring jstr = static_cast<jstring>(env->CallObjectMethod(obj, mid, nullptr));
    return string(env->GetStringUTFChars(jstr, nullptr));
}

void player_init_play(JNIEnv *env, jobject obj) {
    string key = getKey(env, obj);
    LyjPlayer *player = new LyjPlayer();
    // 保存jvm实例用于回调
    env->GetJavaVM(&player->vm);
    player_map[key] = player;
    player->init();
}

// 设置surface
void player_set_surface(JNIEnv *env, jobject obj, jobject surface) {
    string key = getKey(env, obj);
    LyjPlayer *player = player_map[key];
    if (player) {
        // 根据传入surface创建对应ANativeWindow用于显示画面
        ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
        if (!window) {
            LOGE("window null");
        } else {
            player->window = window;
        }
    }
}

// 设置回调
void player_set_callback(JNIEnv *env, jobject obj, jobject callback) {
    string key = getKey(env, obj);
    LyjPlayer *player = player_map[key];
    if (player) {
        if (player->callback) {
            env->DeleteGlobalRef(player->callback);
        }
        player->callback = env->NewGlobalRef(callback);
    }
}

// 开始播放
int player_start_play(JNIEnv *env, jobject obj, jstring url) {
    const char *path = nullptr;
    path = env->GetStringUTFChars(url, nullptr);
    string key = getKey(env, obj);
    LyjPlayer *player = player_map[key];
    if (player) {
        player->stopPlay();
        player->startPlay(path);
    } else {
        LOGE("cant not find player");
    }
    env->ReleaseStringUTFChars(url, path);
    return 0;
}

// 停止播放释放资源
int player_stop_play(JNIEnv *env, jobject obj) {
    string key = getKey(env, obj);
    LyjPlayer *player = player_map[key];
    if (player) {
        player->stopPlay();
    }
    return 0;
}

void player_release(JNIEnv *env, jobject obj) {
    string key = getKey(env, obj);
    LyjPlayer *player = player_map[key];
    if (player) {
        env->DeleteGlobalRef(player->callback);
        player->release();
        player_map.erase(key);
        delete player;
    }
}

// 方法映射
JNINativeMethod player_methods[] = {
        {"initPlayer",       "()V",                                    (void *) player_init_play},
        {"setSurface",       "(Landroid/view/Surface;)V",              (void *) player_set_surface},
        {"setVideoCallBack", "(Lcom/lyj/learnffmpeg/VideoCallBack;)V", (void *) player_set_callback},
        {"startPlay",        "(Ljava/lang/String;)I",                  (void *) player_start_play},
        {"stopPlay",         "()I",                                    (void *) player_stop_play},
        {"release",          "()V",                                    (void *) player_release}
};
// jni注册
int jniRegisterNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *methods,
                             int count) {
    int res = -1;
    jclass cls = env->FindClass(className);
    if (cls != nullptr) {
        int ret = env->RegisterNatives(cls, methods, count);
        if (ret > 0) {
            res = 0;
        }
    }
    env->DeleteLocalRef(cls);
    return res;
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    jint result = -1;
    if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
        return result;
    }
    jniRegisterNativeMethods(env, cls_player, player_methods, arrayLen(player_methods));
    return JNI_VERSION_1_6;
}

JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) {}

#ifdef __cplusplus
}
#endif

播放器主要逻辑

简单思路

  1. 开启一个线程不断取流,对码流进行解码,将解码后的数据放入队列
  2. 根据码流信息计算每一帧时间,开启一个定时任务不断从队列取rgb数据绘制

代码实现

lyjplayer.h

#ifdef __cplusplus
extern "C" {
#endif

#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#ifdef __cplusplus
}
#endif

using namespace std;

struct FrameData {
    AVFrame *frame;
    // 帧的实际数据
    uint8_t *buffer;
};

class LyjPlayer {
private:
    const char *url;
    int width = 0;
    int height = 0;
    atomic_bool playing = {false};
    // AVFormatContext用于解封装 flv,avi,rmvb,mp4
    AVFormatContext *formatContext = nullptr;
    AVCodecContext *codecContext = nullptr;
    int buffer_size;
    AVFrame *frame = nullptr, *temp = nullptr;
    AVPacket *packet = nullptr;
    SwsContext *sws_context = nullptr;
    uint8_t *buffer = nullptr;
    ANativeWindow_Buffer windowBuffer;
    thread task;
    // 记录帧编号
    int index;
    // 网络接收的缓冲
    LinkedBlockingQueue<FrameData> queue;
    // 定时器
    Timer timer;

    int decodeFrame();

    int render();

    int destroyPlay();

    void callbackState(JNIEnv *env, PlayState state);

    void callbackError(JNIEnv *env, PlayError error);
public:
    JavaVM *vm = nullptr;
    jobject callback = nullptr;
    ANativeWindow *window = nullptr;

    LyjPlayer();

    int init();

    void startPlay(const char *url);

    int stopPlay();

    void release();

    virtual ~LyjPlayer();

};
#endif

lyjplayer.cpp

初始化

int LyjPlayer::init() {
    // 由于mediaCodec硬解要用到jni,所以需要调用这个函数
    av_jni_set_java_vm(vm, 0);
    return 0;
}

开始播放

Timer是一个简单的定时任务,每过40ms绘制一帧画面,保持帧率稳定,40ms这个时间根据帧率和视频流的时间基计算出来,这里流的时间基是1/1000,相当于把一秒分成了1000份,一帧占40个时间基也就是40毫秒。

使用mediacodec硬解只需要设置相应的解码器就可以

void LyjPlayer::startPlay(const char *url) {
    this->url = url;
    playing = true;
    if (task.joinable()) {
        task.join();
    }
    // 取流解码线程
    task = thread([this] {
        JNIEnv *env = nullptr;
        int ret = vm->AttachCurrentThread(&env, nullptr);
        avformat_network_init();
        formatContext = avformat_alloc_context();
        // 打开文件
        LOGE("正在连接");
        ret = avformat_open_input(&formatContext, this->url, nullptr, nullptr);
        if (ret < 0) {
            LOGE("打开文件失败code:%d msg:%s", ret, av_err2str(ret));
            callbackError(env, PlayError::CONNECT_TIMEOUT);
            vm->DetachCurrentThread();
            destroyPlay();
            return ret;
        }
        callbackState(env, PlayState::CONNECTED);
        LOGE("连接到流媒体成功");
        ret = avformat_find_stream_info(formatContext, nullptr);
        if (ret < 0) {
            LOGE("查找流失败 %s", av_err2str(ret));
            callbackError(env, PlayError::ERROR_STREAM);
            vm->DetachCurrentThread();
            destroyPlay();
            return ret;
        }
        int index = -1;
        for (int i = 0; i < formatContext->nb_streams; i++) {
            // 查找视频流,如果有音频的话就不止一个流,所以需要查找
            if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                index = i;
                break;
            }
        }
        if (index == -1) {
            LOGE("没有视频流");
            callbackError(env, PlayError::NONE_VIDEO_STREAM);
            vm->DetachCurrentThread();
            destroyPlay();
            return -1;
        }
        AVStream *videoStream = formatContext->streams[index];
        AVCodecParameters *params = videoStream->codecpar;
        LOGE("AVCodecParameters id:%d, width:%d, height%d", params->codec_id, params->width,
             params->height);
        // 查找解码器
        AVCodecID codecId = videoStream->codecpar->codec_id;
        AVCodec *codec = nullptr;
        // 使用h264硬解
        if (codecId == AV_CODEC_ID_H264) {
            codec = avcodec_find_decoder_by_name("h264_mediacodec");
            if (codec == nullptr) {
                LOGE("can not find mediacodec");
                codec = avcodec_find_decoder(codecId);
            } else {
                LOGE("使用硬解");
            }
        }
        if (codec == nullptr) {
            LOGE("找不到解码器");
            callbackError(env, PlayError::UNKNOW);
            vm->DetachCurrentThread();
            destroyPlay();
            return -1;
        }
        codecContext = avcodec_alloc_context3(codec);
        // 复制码流配置到解码器
        avcodec_parameters_to_context(codecContext, videoStream->codecpar);
        ret = avcodec_open2(codecContext, codec, nullptr);
        if (ret < 0) {
            LOGE("初始化解码器失败:%s", av_err2str(ret));
            callbackError(env, PlayError::UNKNOW);
            vm->DetachCurrentThread();
            destroyPlay();
            return -1;
        }
        this->width = codecContext->width;
        this->height = codecContext->height;
        buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 1);
        temp = av_frame_alloc();
        packet = av_packet_alloc();
        // 创建格式转换方式,用于将yuv数据转换为rgba
        sws_context = sws_getContext(width, height, codecContext->pix_fmt, width,
                                     height, AV_PIX_FMT_RGBA, SWS_BICUBIC,
                                     nullptr, nullptr, nullptr);
        // 设置窗口参数
        if (ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888) < 0) {
            callbackError(env, PlayError::UNKNOW);
            vm->DetachCurrentThread();
            destroyPlay();
            LOGE("初始化播放窗口失败");
            return -1;
        }
        // 获取帧率
        double fps = av_q2d(videoStream->avg_frame_rate);
        AVRational timebase = videoStream->time_base;
        // 计算每一帧持续时间 毫秒
        int duration = static_cast<int>(timebase.den / timebase.num / fps / (timebase.den / 1000));
        LOGE("videoStream FPS %lf, duration %d", fps, duration);
        // 定时绘制,保持帧率
        timer.setInterval([this] {
            // 绘制一帧
            render();
        }, duration);
        while (playing) {
            // 读流
            ret = av_read_frame(formatContext, packet);
            if (ret < 0) {
                continue;
            }
            if (packet->stream_index == index) {
                // 解码一帧
                decodeFrame();
            }
            av_packet_unref(packet);
        }
        vm->DetachCurrentThread();
        return 0;
    });
}

decodeFrame 解码数据

int LyjPlayer::decodeFrame() {
    int ret = avcodec_send_packet(codecContext, packet);
    if (ret == AVERROR(EAGAIN)) {
        ret = 0;
    } else if (ret < 0) {
        LOGE("avcodec_send_packet err code: %d, msg:%s", ret, av_err2str(ret));
        av_packet_free(&packet);
        vm->DetachCurrentThread();
        destroyPlay();
        return -1;
    }
    LOGE("send a packet");
    while (ret >= 0) {
        ret = avcodec_receive_frame(codecContext, temp);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // packet已读完
            return 0;
        } else if (ret < 0) {
            LOGE("avcodec_receive_frame error %s", av_err2str(ret));
            av_packet_free(&packet);
            vm->DetachCurrentThread();
            destroyPlay();
            return -1;
        }
        AVFrame *frame = av_frame_alloc();
        uint8_t *buffer = static_cast<uint8_t *>(av_malloc(buffer_size));
        av_image_fill_arrays(frame->data, frame->linesize, buffer, AV_PIX_FMT_RGBA, width, height,
                             1);
        // 将frame数据转为rgba格式
        sws_scale(sws_context, temp->data, temp->linesize, 0, codecContext->height,
                  frame->data, frame->linesize);
        FrameData frameData = {frame, buffer};
        queue.push(frameData);
    }
    return ret;
}

显示每一帧的画面

int LyjPlayer::render() {
    int ret = 0;
    JNIEnv *env = nullptr;
    FrameData frameData = queue.pop();
    AVFrame *frame = frameData.frame;
    uint8_t *buffer = frameData.buffer;
    // 开始绘制第一帧回调
    if (index == 0) {
        ret = vm->AttachCurrentThread(&env, nullptr);
        callbackState(env, PlayState::START);
    }
    index++;
    ret = ANativeWindow_lock(window, &windowBuffer, nullptr);
    if (ret < 0) {
        LOGE("cannot lock window");
    } else {
        uint8_t *bufferBits = (uint8_t *) windowBuffer.bits;
        // 逐行复制,显示画面其实就是把rgba数据逐行复制到ANativeWindow中的byte数组里
        for (int h = 0; h < height; h++) {
            // rgba四通道,每个像素需要4byte,所以需要stride*4
            memcpy(bufferBits + h * windowBuffer.stride * 4,
                   buffer + h * frame->linesize[0],
                   static_cast<size_t>(frame->linesize[0]));
        }
        ANativeWindow_unlockAndPost(window);
    }
    av_free(buffer);
    av_frame_free(&frame);
    if (env) {
        vm->DetachCurrentThread();
    }
    return ret;
}

整个流程基本就是这个样子,以上代码就是拉流播放的主要代码,注释都写得比较清楚,这里就不再赘述,其他代码可以到demo里看;可以发现其实推流拉流用到的api都差不多,主要是顺序的变化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容