IJKPLAYER源码分析-mediacodec硬解

news2024/11/20 4:38:22

前言

    近期腾出了点时间,拟对IJKPLAYER做更完整的源码分析,并对关键实现细节,作为技术笔记,记录下来。包括Android端硬解码/AudioTrack/OpenSL播放,以及iOS端硬解码/AudioUnit播放,以及OpenGL渲染和Android/iOS端的图像显示技术,不一而足。

    本文着重分析Android端mediacodec硬解实现,由于audio用的是ffmpeg软解方案,因此本文只介绍用mediacodec硬解视频。

mediacodec简介

初识mediacodec

    mediacodec技术是Android系统音视频硬编/硬解的一套标准,各硬件厂商加以实现,经常和MediaExtractor/MediaSync/MediaMuxer/MediaCrypto/MediaDrm/Image/Surface/AudioTrack一起使用。具体请参考google官方文档。

    由上图了解到,一言以蔽之,一端输入、一端输出,input输入的数据提交给codec异步处理后,由output输出再归还给codec。输入的是raw video/audio数据,则输出的是编码后的video/audio数据,输入的是编码后的video/audio数据,则输出的是raw的video/audio数据。

mediacodec状态机

     上图显示,Stopped状态包含三个子状态:Uninitialize/Configured/Error;Executing同样包含三个子状态:Flushed,/Running/End-of-Stream.

  • 对mediacodec的使用需遵守上图所示流程,否则会发生错误。
  • 以decode为例,当创建了mediacodec并且指定为解码后,进入Uninitialized状态,调用configure方法后,进入Configured状态,再调用start方法进入Executing状态。
  • 进入Executing状态后,首先到达Flush状态,此时mediacodec会持有所有的数据,当第一个inputbufffer从队列中取出时,立即进入Running状态,这个时间很短。然后就可以调用dequeueInputBuffer和getInputBuffer来获取用户可用的缓冲区,用户填满数据后调用queueinputbuffer方法提交给解码器,解码器大部分时间都会工作在Running状态。当想inputbufferqueue中输入一帧标记EndOfStream的时候,进入End-of-Stream状态,此时,解码器不再接受任何新的数据输入,缓冲区中的数据和标记EndOfStream最终会执行完毕。在任何时候都可以调用flush方法回到Flush状态。
  • 调用stop方法会使mediacodec进入Uninitialized状态,这时候可以执行configure方法来进入下一循环。当mediacodec使用完毕后必须调用release方法来释放所有的资源。
  • 在某些情况下,如取出缓冲区索引时,mediacodec会发生错误进入Error状态,此时调用reset方法使得mediacodec重新处于Uninitialized状态,或者调用release来结束解码。

input

    此处以解码为例,一般的解码操作如下:

  • 通过inputBuffer提交给mediacodec的数据,应当是1个完整帧;
  • 若不是完整帧,则解码后会马赛克、画面显示异常,或在API 26以后可通过queueInputBuffer方法设置标志位BUFFER_FLAG_PARTIAL_FRAME告诉mediacodec是部分帧,由mediacodec组装成1个完整的帧再解码;

output 

  • 通过releaseOutputBuffer(codec, output_buffer_index, render)来控制此帧显示与否,并归还output_buffer_index的缓冲区;

硬解码

何为硬解

    所谓软解,是指使用CPU进行解码运算,GPU用以视频渲染加速,而硬解则是指利用移动端设备DSP芯片的解码能力进行解码。

    此外,必须说明的是,IJKPLAYER所支持的mediacodec硬解,是在native层以反射调用java层mediacodec的硬解码能力。

    为何没用NDK的mediacodec解码能力?原因在于NDK的硬解能力是在API 21(Android 5.0)后才得以支持。

Android SDK硬解

感性认识

     为了让各位对mediacodec有个感性认识,以下是我用c写的伪代码,大致描述了用jmediacodec解码播放的流程:

// use mediacodec api 伪代码
void decodeWithMediacodec() {
    // create h26x decoder
    MediaCodec codec = MediaCodec.createByCodecName("video/avc");

    // configure
    codec.configure(codec, android_media_format, android_surface, crypto, flags);

    // start
    codec.start();

    // dequeuInputBuffer
    int input_buffer_index = codec.dequeInputBuffer(codec, timeout);

    // write copy_size pixel data to input_buffer_index buffer
    ......

    // queueInputBuffer
    codec.queueInputBuffer(acodec, input_buffer_index, 0, copy_size, time_stamp, queue_flags)

    // dequeOutputBuffer
    int output_buffer_index = codec.dequeOutputBuffer(codec, bufferInfo, timeout);

    // releaseOutputBuffer
    codec.releaseOutputBuffer(codec, output_buffer_index, true);

    // flush
    codec.flush()

    // stop / release
    codec.stop() or codec.release()
}

Annex-b格式

    在进入硬解流程之前,先来认识下annex-b格式的h26x码流:

  • 所谓的annex-b格式码流,是指用0x00000001或0x000001起始码分割的nalu单元,是一种h26x的一种码流组织方式;
  • 具体如:| start_code | nalu header | nalu | start_code | nalu header | nalu | ......
  • mediacodec解码所需的数据必须是带start_code的nalu数据,即| start_code | nalu header | nalu

 硬解流程

    上一节用伪代码描述了对mediacodec的简易使用,此处便可以正式介绍IJKPLAYER对mediacodec的使用了。归纳起来,对mediacodec的使用有如下步骤:

  • 首先,创建对应解码器,并完成对surface等的配置;
  • 调用start()启动mediacodec解码;
  • 调用dequeInputBuffer取得empty input buffer index,完成数据的写入;
  • 再用queueInputBuffer()提交给mediacodec解码;
  • 再用dequeOutputBuffer()取得output buffer index;
  • 再用releaseOutputBuffer(codec, output_buffer_index, true)完成视频的绘制;

    业务侧使能了mediacodec选项后,创建mediacodec video decoder时机,是在read_thread线程里通过avformat_find_stream_info()接口拿到视频的参数之后:

read_thread() => stream_component_open() => ffpipeline_open_video_decoder() => func_open_video_decoder() => ffpipenode_create_video_decoder_from_android_mediacodec()

    此处根据业务侧所设置的选项,选用ffmpeg软解,还是mediacodec硬解: 

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;

    // 业务侧使能了mediacodec选项,则使用之
    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }

    return node;
}

     完成mediacodec的解码器的创建,surface的配置,并启动解码器:

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout)
{
    ALOGD("ffpipenode_create_video_decoder_from_android_mediacodec()\n");
    if (SDL_Android_GetApiLevel() < IJK_API_16_JELLY_BEAN)
        return NULL;

    if (!ffp || !ffp->is)
        return NULL;

    IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
    if (!node)
        return node;

    VideoState            *is     = ffp->is;
    IJKFF_Pipenode_Opaque *opaque = node->opaque;
    JNIEnv                *env    = NULL;
    int                    ret    = 0;
    jobject                jsurface = NULL;

    node->func_destroy  = func_destroy;
    if (ffp->mediacodec_sync) {
        node->func_run_sync = func_run_sync_loop;
    } else {
        node->func_run_sync = func_run_sync;
    }
    node->func_flush    = func_flush;
    opaque->pipeline    = pipeline;
    opaque->ffp         = ffp;
    opaque->decoder     = &is->viddec;
    opaque->weak_vout   = vout;

    opaque->codecpar = avcodec_parameters_alloc();
    if (!opaque->codecpar)
        goto fail;

    ret = avcodec_parameters_from_context(opaque->codecpar, opaque->decoder->avctx);
    if (ret)
        goto fail;

    // 根据opaque->codecpar->codec_id取得profile和level以及java层创建decoder时所需的mime_type,如“video/avc”,此处略去相关代码
    ......

    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s:create: SetupThreadEnv failed\n", __func__);
        goto fail;
    }

    opaque->acodec_mutex                      = SDL_CreateMutex();
    opaque->acodec_cond                       = SDL_CreateCond();
    opaque->acodec_first_dequeue_output_mutex = SDL_CreateMutex();
    opaque->acodec_first_dequeue_output_cond  = SDL_CreateCond();
    opaque->any_input_mutex                   = SDL_CreateMutex();
    opaque->any_input_cond                    = SDL_CreateCond();

    if (!opaque->acodec_cond || !opaque->acodec_cond || !opaque->acodec_first_dequeue_output_mutex || !opaque->acodec_first_dequeue_output_cond) {
        ALOGE("%s:open_video_decoder: SDL_CreateCond() failed\n", __func__);
        goto fail;
    }

    // 创建inputFormat,并通过setBuffer将sps和pps设置给mediacodec
    ret = recreate_format_l(env, node);
    if (ret) {
        ALOGE("amc: recreate_format_l failed\n");
        goto fail;
    }

    if (!ffpipeline_select_mediacodec_l(pipeline, &opaque->mcc) || !opaque->mcc.codec_name[0]) {
        ALOGE("amc: no suitable codec\n");
        goto fail;
    }

    jsurface = ffpipeline_get_surface_as_global_ref(env, pipeline);
    // 创建decoder,完成surface配置,并启动mediacodec解码
    ret = reconfigure_codec_l(env, node, jsurface);
    J4A_DeleteGlobalRef__p(env, &jsurface);
    if (ret != 0)
        goto fail;

    ffp_set_video_codec_info(ffp, MEDIACODEC_MODULE_NAME, opaque->mcc.codec_name);

    opaque->off_buf_out = 0;
    if (opaque->n_buf_out) {
        int i;

        opaque->amc_buf_out = calloc(opaque->n_buf_out, sizeof(*opaque->amc_buf_out));
        assert(opaque->amc_buf_out != NULL);
        for (i = 0; i < opaque->n_buf_out; i++)
            opaque->amc_buf_out[i].pts = AV_NOPTS_VALUE;
    }

    SDL_SpeedSamplerReset(&opaque->sampler);
    ffp->stat.vdec_type = FFP_PROPV_DECODER_MEDIACODEC;
    return node;
fail:
    ffpipenode_free_p(&node);
    return NULL;
}

    mediacodec要能解码,必须先拿到sps/pps等视频参数信息,方式有二:

  • 在configure之前,通过mediacodec的setBuffer,csd-0或csd-1属性,将sps和pps提交给mediacodec;
  • 在start启动解码器后,通过queueInputBuffer()提交sps和pps时,位或flag |= AMEDIACODEC__BUFFER_FLAG_CODEC_CONFIG亦可;
  • 值得一提的是,若播放过程中视频的分辨率发生变更,需重新configure完成配置并提交新的sps和pps数据;

    IJKPLAYER选择了第一种方式:

static int recreate_format_l(JNIEnv *env, IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque *opaque         = node->opaque;
    FFPlayer              *ffp            = opaque->ffp;
    int                    rotate_degrees = 0;

    ALOGI("AMediaFormat: %s, %dx%d\n", opaque->mcc.mime_type, opaque->codecpar->width, opaque->codecpar->height);
    SDL_AMediaFormat_deleteP(&opaque->output_aformat);
    opaque->input_aformat = SDL_AMediaFormatJava_createVideoFormat(env, opaque->mcc.mime_type, opaque->codecpar->width, opaque->codecpar->height);
    if (opaque->codecpar->extradata && opaque->codecpar->extradata_size > 0) {
        if ((opaque->codecpar->codec_id == AV_CODEC_ID_H264 && opaque->codecpar->extradata[0] == 1)
            || (opaque->codecpar->codec_id == AV_CODEC_ID_HEVC && opaque->codecpar->extradata_size > 3
                && (opaque->codecpar->extradata[0] == 1 || opaque->codecpar->extradata[1] == 1))) {
#if AMC_USE_AVBITSTREAM_FILTER
            // 此处使用h264_mp4toannexb或hevc_mp4toannexb过滤器将avcC格式的数据转为annex-b格式,略去相关code
            ......
#else
            size_t   sps_pps_size   = 0;
            size_t   convert_size   = opaque->codecpar->extradata_size + 20;
            uint8_t *convert_buffer = (uint8_t *)calloc(1, convert_size);
            if (!convert_buffer) {
                ALOGE("%s:sps_pps_buffer: alloc failed\n", __func__);
                goto fail;
            }
            if (opaque->codecpar->codec_id == AV_CODEC_ID_H264) {
                if (0 != convert_sps_pps(opaque->codecpar->extradata, opaque->codecpar->extradata_size,
                                         convert_buffer, convert_size,
                                         &sps_pps_size, &opaque->nal_size)) {
                    ALOGE("%s:convert_sps_pps: failed\n", __func__);
                    goto fail;
                }
            } else {
                if (0 != convert_hevc_nal_units(opaque->codecpar->extradata, opaque->codecpar->extradata_size,
                                                convert_buffer, convert_size,
                                                &sps_pps_size, &opaque->nal_size)) {
                    ALOGE("%s:convert_hevc_nal_units: failed\n", __func__);
                    goto fail;
                }
            }
            // 将sps/pps设置给mediacodec,通过csd-0,start_code|sps|start_code|pps
            SDL_AMediaFormat_setBuffer(opaque->input_aformat, "csd-0", convert_buffer, sps_pps_size);
            for(int i = 0; i < sps_pps_size; i+=4) {
                ALOGE("csd-0[%d]: %02x%02x%02x%02x\n", (int)sps_pps_size, (int)convert_buffer[i+0], (int)convert_buffer[i+1], (int)convert_buffer[i+2], (int)convert_buffer[i+3]);
            }
            free(convert_buffer);
#endif
        } else if (opaque->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
            // ......此处省去MPEG4格式
        } else {
            // Codec specific data
            // SDL_AMediaFormat_setBuffer(opaque->aformat, "csd-0", opaque->codecpar->extradata, opaque->codecpar->extradata_size);
            ALOGE("csd-0: naked\n");
        }
    } else {
        ALOGE("no buffer(%d)\n", opaque->codecpar->extradata_size);
    }

    // 通过ffmpeg拿到旋转角度,再设置给mediacodec,让mediacodec自动旋转
    rotate_degrees = ffp_get_video_rotate_degrees(ffp);
    if (ffp->mediacodec_auto_rotate &&
        rotate_degrees != 0 &&
        SDL_Android_GetApiLevel() >= IJK_API_21_LOLLIPOP) {
        ALOGI("amc: rotate in decoder: %d\n", rotate_degrees);
        opaque->frame_rotate_degrees = rotate_degrees;
        SDL_AMediaFormat_setInt32(opaque->input_aformat, "rotation-degrees", rotate_degrees);
        ffp_notify_msg2(ffp, FFP_MSG_VIDEO_ROTATION_CHANGED, 0);
    } else {
        ALOGI("amc: rotate notify: %d\n", rotate_degrees);
        ffp_notify_msg2(ffp, FFP_MSG_VIDEO_ROTATION_CHANGED, rotate_degrees);
    }

    return 0;
fail:
    return -1;
}

    万事俱备,可以给mediacodec喂数据了: 

static int feed_input_buffer(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *enqueue_count)
{
    IJKFF_Pipenode_Opaque *opaque   = node->opaque;
    FFPlayer              *ffp      = opaque->ffp;
    IJKFF_Pipeline        *pipeline = opaque->pipeline;
    VideoState            *is       = ffp->is;
    Decoder               *d        = &is->viddec;
    PacketQueue           *q        = d->queue;
    sdl_amedia_status_t    amc_ret  = 0;
    int                    ret      = 0;
    ssize_t  input_buffer_index = 0;
    ssize_t  copy_size          = 0;
    int64_t  time_stamp         = 0;
    uint32_t queue_flags        = 0;

    if (enqueue_count)
        *enqueue_count = 0;

    if (d->queue->abort_request) {
        ret = 0;
        goto fail;
    }

    if (!d->packet_pending || d->queue->serial != d->pkt_serial) {
#if AMC_USE_AVBITSTREAM_FILTER
#else
        H264ConvertState convert_state = {0, 0};
#endif
        // 从AVPacket队列里拿到1帧视频数据,压缩的数据
        AVPacket pkt;
        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (ffp_packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0) {
                ret = -1;
                goto fail;
            }
            if (ffp_is_flush_packet(&pkt) || opaque->acodec_flush_request) {
                // request flush before lock, or never get mutex
                opaque->acodec_flush_request = true;
                SDL_LockMutex(opaque->acodec_mutex);
                if (SDL_AMediaCodec_isStarted(opaque->acodec)) {
                    if (opaque->input_packet_count > 0) {
                        // flush empty queue cause error on OMX.SEC.AVC.Decoder (Nexus S)
                        SDL_VoutAndroid_invalidateAllBuffers(opaque->weak_vout);
                        SDL_AMediaCodec_flush(opaque->acodec);
                        opaque->input_packet_count = 0;
                    }
                    // If codec is configured in synchronous mode, codec will resume automatically
                    // SDL_AMediaCodec_start(opaque->acodec);
                }
                opaque->acodec_flush_request = false;
                SDL_CondSignal(opaque->acodec_cond);
                SDL_UnlockMutex(opaque->acodec_mutex);
                d->finished = 0;
                d->next_pts = d->start_pts;
                d->next_pts_tb = d->start_pts_tb;
            }
        } while (ffp_is_flush_packet(&pkt) || d->queue->serial != d->pkt_serial);
        av_packet_split_side_data(&pkt);
        av_packet_unref(&d->pkt);
        d->pkt_temp = d->pkt = pkt;
        d->packet_pending = 1;

        if (opaque->ffp->mediacodec_handle_resolution_change &&
            opaque->codecpar->codec_id == AV_CODEC_ID_H264) {
            // 此处处理分辨率变更的逻辑,略去相关代码
            ......
        }

#if AMC_USE_AVBITSTREAM_FILTER
        // d->pkt_temp->data could be allocated by av_bitstream_filter_filter
        // 此处使用av_bitstream_filter_filter过滤器将avcC格式的数据转为annex-b格式
        ......
#else

        // 将avcC格式数据转为annex-b格式的
        if (opaque->codecpar->codec_id == AV_CODEC_ID_H264 || opaque->codecpar->codec_id == AV_CODEC_ID_HEVC) {
            convert_h264_to_annexb(d->pkt_temp.data, d->pkt_temp.size, opaque->nal_size, &convert_state);
            int64_t time_stamp = d->pkt_temp.pts;
            if (!time_stamp && d->pkt_temp.dts)
                time_stamp = d->pkt_temp.dts;
            if (time_stamp > 0) {
                time_stamp = av_rescale_q(time_stamp, is->video_st->time_base, AV_TIME_BASE_Q);
            } else {
                time_stamp = 0;
            }
        }

#endif
    }

    if (d->pkt_temp.data) {
        // reconfigure surface if surface changed
        // NULL surface cause no display
        // 此处是重新配置surface,重启解码器的逻辑,略去相关代码
        ......


        // 拿到mediacodec的一个empty input buffer index
        queue_flags = 0;
        input_buffer_index = SDL_AMediaCodec_dequeueInputBuffer(opaque->acodec, timeUs);
        if (input_buffer_index < 0) {
            if (SDL_AMediaCodec_isInputBuffersValid(opaque->acodec)) {
                // timeout
                ret = 0;
                goto fail;
            } else {
                // enqueue fake frame
                queue_flags |= AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME;
                copy_size    = d->pkt_temp.size;
            }
        } else {
            SDL_AMediaCodecFake_flushFakeFrames(opaque->acodec);
            // 将video数据写入empty input buffer
            copy_size = SDL_AMediaCodec_writeInputData(opaque->acodec, input_buffer_index, d->pkt_temp.data, d->pkt_temp.size);
            if (!copy_size) {
                ALOGE("%s: SDL_AMediaCodec_getInputBuffer failed\n", __func__);
                ret = -1;
                goto fail;
            }
        }

        time_stamp = d->pkt_temp.pts;
        if (time_stamp == AV_NOPTS_VALUE && d->pkt_temp.dts != AV_NOPTS_VALUE)
            time_stamp = d->pkt_temp.dts;
        if (time_stamp >= 0) {
            time_stamp = av_rescale_q(time_stamp, is->video_st->time_base, AV_TIME_BASE_Q);
        } else {
            time_stamp = 0;
        }
        // ALOGE("queueInputBuffer, %lld\n", time_stamp);
        // 将video数据input_buffer_index所指向的内容提交给mediacodec
        amc_ret = SDL_AMediaCodec_queueInputBuffer(opaque->acodec, input_buffer_index, 0, copy_size, time_stamp, queue_flags);
        if (amc_ret != SDL_AMEDIA_OK) {
            ALOGE("%s: SDL_AMediaCodec_getInputBuffer failed\n", __func__);
            ret = -1;
            goto fail;
        }
        // ALOGE("%s: queue %d/%d", __func__, (int)copy_size, (int)input_buffer_size);
        opaque->input_packet_count++;
        if (enqueue_count)
            ++*enqueue_count;
    }

    if (copy_size < 0) {
        d->packet_pending = 0;
    } else {
        d->pkt_temp.dts =
        d->pkt_temp.pts = AV_NOPTS_VALUE;
        if (d->pkt_temp.data) {
            // 1个AVPacket的数据,1个mediacodec的inputBuffer或装不下,需将剩下的数据装入下1个或下下1个inputBuffer中,直到装完
            // 或引发解码异常,画面马赛克
            d->pkt_temp.data += copy_size;
            d->pkt_temp.size -= copy_size;
            if (d->pkt_temp.size <= 0)
                d->packet_pending = 0;
        } else {
            // FIXME: detect if decode finished
            // if (!got_frame) {
                d->packet_pending = 0;
                // 到文件末,解码完毕
                d->finished = d->pkt_serial;
            // }
        }
    }

fail:
    return ret;
}

    消费mediacodec的像素数据,控制video的显示:

static int drain_output_buffer2(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, AVRational frame_rate)
{
    IJKFF_Pipenode_Opaque *opaque    = node->opaque;
    FFPlayer              *ffp       = opaque->ffp;
    VideoState            *is        = ffp->is;
    AVRational            tb         = is->video_st->time_base;
    int                   got_frame  = 0;
    int                   ret        = -1;
    double                duration;
    double                pts;
    while (ret) {
        got_frame = 0;
        // 消费mediacodec的output buffer,控制显示
        ret = drain_output_buffer2_l(env, node, timeUs, dequeue_count, frame, &got_frame);

        if (opaque->decoder->queue->abort_request) {
            if (got_frame && frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);

            return ACODEC_EXIT;
        }

        if (ret != 0) {
            if (got_frame && frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
        }
    }

    // AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME帧似只在此处有用
    if (got_frame) {
        // 此处若业务侧使能了framedrop选项,有丢帧逻辑,略去相关代码
        ......

        // 统一ffmpeg软解render逻辑,此处将mediacodec解码后的AVFrame填充之后,喂给OpenGL准备渲染
        ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
        if (ret) {
            if (frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
        }
        av_frame_unref(frame);
    }

    return ret;
}

    消费output buffer数据,填充AVFrame,统一ffmpeg软解render逻辑:

static int drain_output_buffer2(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, AVRational frame_rate)
{
    IJKFF_Pipenode_Opaque *opaque    = node->opaque;
    FFPlayer              *ffp       = opaque->ffp;
    VideoState            *is        = ffp->is;
    AVRational            tb         = is->video_st->time_base;
    int                   got_frame  = 0;
    int                   ret        = -1;
    double                duration;
    double                pts;
    while (ret) {
        got_frame = 0;
        // 消费mediacodec的output buffer,填充AVFrame,并控制显示
        ret = drain_output_buffer2_l(env, node, timeUs, dequeue_count, frame, &got_frame);

        if (opaque->decoder->queue->abort_request) {
            if (got_frame && frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);

            return ACODEC_EXIT;
        }

        if (ret != 0) {
            if (got_frame && frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
        }
    }

    // AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME帧似只在此处有用
    if (got_frame) {
        // 此处若业务侧使能了framedrop选项,有丢帧逻辑,略去相关代码
        ......

        // 统一ffmpeg软解render逻辑,此处将mediacodec解码后的AVFrame填充之后,喂给OpenGL准备渲染
        ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
        if (ret) {
            if (frame->opaque)
                SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
        }
        av_frame_unref(frame);
    }

    return ret;
}


static int drain_output_buffer2_l(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, int *got_frame)
{
    IJKFF_Pipenode_Opaque *opaque         = node->opaque;
    FFPlayer              *ffp            = opaque->ffp;
    SDL_AMediaCodecBufferInfo bufferInfo;
    ssize_t          output_buffer_index  = 0;

    if (dequeue_count)
        *dequeue_count = 0;

    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s:create: SetupThreadEnv failed\n", __func__);
        return ACODEC_RETRY;
    }

    // 取得output buffer index,控制渲染
    output_buffer_index = SDL_AMediaCodecFake_dequeueOutputBuffer(opaque->acodec, &bufferInfo, timeUs);
    if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED) {
        ALOGD("AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED\n");
        return ACODEC_RETRY;
    } else if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED) {
        ALOGD("AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED\n");
        SDL_AMediaFormat_deleteP(&opaque->output_aformat);
        // 视频的输出格式发生变更,此处打印之
        opaque->output_aformat = SDL_AMediaCodec_getOutputFormat(opaque->acodec);
        if (opaque->output_aformat) {
            int width        = 0;
            int height       = 0;
            int color_format = 0;
            int stride       = 0;
            int slice_height = 0;
            int crop_left    = 0;
            int crop_top     = 0;
            int crop_right   = 0;
            int crop_bottom  = 0;

            SDL_AMediaFormat_getInt32(opaque->output_aformat, "width",          &width);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "height",         &height);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "color-format",   &color_format);

            SDL_AMediaFormat_getInt32(opaque->output_aformat, "stride",         &stride);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "slice-height",   &slice_height);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-left",      &crop_left);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-top",       &crop_top);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-right",     &crop_right);
            SDL_AMediaFormat_getInt32(opaque->output_aformat, "crop-bottom",    &crop_bottom);

            // TI decoder could crash after reconfigure
            // ffp_notify_msg3(ffp, FFP_MSG_VIDEO_SIZE_CHANGED, width, height);
            // opaque->frame_width  = width;
            // opaque->frame_height = height;
            ALOGI(
                "AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED\n"
                "    width-height: (%d x %d)\n"
                "    color-format: (%s: 0x%x)\n"
                "    stride:       (%d)\n"
                "    slice-height: (%d)\n"
                "    crop:         (%d, %d, %d, %d)\n"
                ,
                width, height,
                SDL_AMediaCodec_getColorFormatName(color_format), color_format,
                stride,
                slice_height,
                crop_left, crop_top, crop_right, crop_bottom);
        }
        return ACODEC_RETRY;
        // continue;
    } else if (output_buffer_index == AMEDIACODEC__INFO_TRY_AGAIN_LATER) {
        return 0;
        // continue;
    } else if (output_buffer_index < 0) {
        return 0;
    } else if (output_buffer_index >= 0) {
        ffp->stat.vdps = SDL_SpeedSamplerAdd(&opaque->sampler, FFP_SHOW_VDPS_MEDIACODEC, "vdps[MediaCodec]");

        if (dequeue_count)
            ++*dequeue_count;

        // Galaxy Nexus, Ti OMAP4460 手机兼容
        if (opaque->n_buf_out) {
            // 此处是mediacodec对以上手机的兼容处理,略去相关代码
            ......
        } else {
            // 此处将mediacodec解码后的output buffer index的内容填充到AVFrame
            amc_fill_frame(node, frame, got_frame, output_buffer_index, SDL_AMediaCodec_getSerial(opaque->acodec), &bufferInfo);
        }
    }

    return 0;
}

     最后在此处render,由于mediacodec使用的是ANativeWindow,是surface输出,不需要在native层再用OpenGL ES渲染了:

static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    SDL_Vout_Opaque *opaque = vout->opaque;
    ANativeWindow *native_window = opaque->native_window;

    if (!native_window) {
        if (!opaque->null_native_window_warned) {
            opaque->null_native_window_warned = 1;
            ALOGW("func_display_overlay_l: NULL native_window");
        }
        return -1;
    } else {
        opaque->null_native_window_warned = 1;
    }

    if (!overlay) {
        ALOGE("func_display_overlay_l: NULL overlay");
        return -1;
    }

    if (overlay->w <= 0 || overlay->h <= 0) {
        ALOGE("func_display_overlay_l: invalid overlay dimensions(%d, %d)", overlay->w, overlay->h);
        return -1;
    }

    switch(overlay->format) {
    case SDL_FCC__AMC: {
        // only ANativeWindow support
        IJK_EGL_terminate(opaque->egl);
        // mediacodec硬解的render在此函数里
        return SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, true);
    }
    case SDL_FCC_RV24:
    case SDL_FCC_I420:
    case SDL_FCC_I444P10LE: {
        // only GLES support
        if (opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    case SDL_FCC_YV12:
    case SDL_FCC_RV16:
    case SDL_FCC_RV32: {
        // both GLES & ANativeWindow support
        if (vout->overlay_format == SDL_FCC__GLES2 && opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    }

    // fallback to ANativeWindow
    IJK_EGL_terminate(opaque->egl);
    return SDL_Android_NativeWindow_display_l(native_window, overlay); 
}

    之后的调用链: 

SDL_VoutOverlayAMediaCodec_releaseFrame_l() => SDL_VoutAndroid_releaseBufferProxyP_l() => SDL_VoutAndroid_releaseBufferProxy_l()

     最后,走到此处render:

static int SDL_VoutAndroid_releaseBufferProxy_l(SDL_Vout *vout, SDL_AMediaCodecBufferProxy *proxy, bool render)
{
    SDL_Vout_Opaque *opaque = vout->opaque;

    if (!proxy)
        return 0;

    AMCTRACE("%s: [%d] -------- proxy %d: vout: %d idx: %d render: %s fake: %s",
        __func__,
        proxy->buffer_id,
        proxy->acodec_serial,
        SDL_AMediaCodec_getSerial(opaque->acodec),
        proxy->buffer_index,
        render ? "true" : "false",
        (proxy->buffer_info.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME) ? "YES" : "NO");
    ISDL_Array__push_back(&opaque->overlay_pool, proxy);

    if (!SDL_AMediaCodec_isSameSerial(opaque->acodec, proxy->acodec_serial)) {
        ALOGW("%s: [%d] ???????? proxy %d: vout: %d idx: %d render: %s fake: %s",
            __func__,
            proxy->buffer_id,
            proxy->acodec_serial,
            SDL_AMediaCodec_getSerial(opaque->acodec),
            proxy->buffer_index, 
            render ? "true" : "false",
            (proxy->buffer_info.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME) ? "YES" : "NO");
        return 0;
    }

    if (proxy->buffer_index < 0) {
        ALOGE("%s: [%d] invalid AMediaCodec buffer index %d\n", __func__, proxy->buffer_id, proxy->buffer_index);
        return 0;
    } else if (proxy->buffer_info.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME) {
        proxy->buffer_index = -1;
        return 0;
    }

    // 在此处控制最后render与否
    sdl_amedia_status_t amc_ret = SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, proxy->buffer_index, render);    
    if (amc_ret != SDL_AMEDIA_OK) {
        ALOGW("%s: [%d] !!!!!!!! proxy %d: vout: %d idx: %d render: %s, fake: %s",
            __func__,
            proxy->buffer_id,
            proxy->acodec_serial,
            SDL_AMediaCodec_getSerial(opaque->acodec),
            proxy->buffer_index, 
            render ? "true" : "false",
            (proxy->buffer_info.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME) ? "YES" : "NO");
        proxy->buffer_index = -1;
        return -1;
    }
    proxy->buffer_index = -1;

    return 0;
}

分辨率改变

    // TODO:待增加

SEEK时

    由IJKPLAYER源码分析-点播原理-CSDN博客一文讨论可知,seek时应将mediacodec解码器的缓存flush掉,准备render seek后的video画面:

    for (;;) {
        // 此处省去不相干代码
        ......

	    if (is->seek_req) {
            int64_t seek_target = is->seek_pos;
            int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
            int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
//      of the seek_pos/seek_rel variables

            ffp_toggle_buffering(ffp, 1);
            ffp_notify_msg3(ffp, FFP_MSG_BUFFERING_UPDATE, 0, 0);
            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR,
                       "%s: error while seeking\n", is->ic->filename);
            } else {
                if (is->audio_stream >= 0) {
                    packet_queue_flush(&is->audioq);
                    packet_queue_put(&is->audioq, &flush_pkt);
                    // TODO: clear invaild audio data
                    // SDL_AoutFlushAudio(ffp->aout);
                }
                if (is->subtitle_stream >= 0) {
                    packet_queue_flush(&is->subtitleq);
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
                if (is->video_stream >= 0) {
                    if (ffp->node_vdec) {
                        // 将mediacodec 硬解码器缓存flush掉
                        ffpipenode_flush(ffp->node_vdec);
                    }
                    packet_queue_flush(&is->videoq);
                    packet_queue_put(&is->videoq, &flush_pkt);
                }
            }
        }
    }

    调用链: 

ffpipenode_flush(node) => SDL_AMediaCodecJava_flush(codec)

     在此反射到Java层调用mediacodec的flush方法,将解码器的缓存清空:

static sdl_amedia_status_t SDL_AMediaCodecJava_flush(SDL_AMediaCodec* acodec)
{
    SDLTRACE("%s", __func__);

    JNIEnv *env = NULL;
    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s: SetupThreadEnv failed", __func__);
        return SDL_AMEDIA_ERROR_UNKNOWN;
    }

    jobject android_media_codec = SDL_AMediaCodecJava_getObject(env, acodec);
    // 将mediacodec的解码器缓存flush掉
    J4AC_MediaCodec__flush(env, android_media_codec);
    if (J4A_ExceptionCheck__catchAll(env)) {
        ALOGE("%s: flush", __func__);
        return SDL_AMEDIA_ERROR_UNKNOWN;
    }

    acodec->object_serial = SDL_AMediaCodec_create_object_serial();
    return SDL_AMEDIA_OK;
}

异常处理

    mediacodec的异常处理,本文主要讨论喂给解码器部分帧和dequeInputBuffer异常。

部分帧

    在喂数据给mediacodec解码器时,或因InputBuffer容量不够,而不能提交1个完整帧的情况。IJKPLAYER的处理方式是按序多次提交部分帧,直到完整提交为止:

    调用链:

feed_input_buffer() => SDL_AMediaCodec_writeInputData() => SDL_AMediaCodecJava_writeInputData()
static ssize_t SDL_AMediaCodecJava_writeInputData(SDL_AMediaCodec* acodec, size_t idx, const uint8_t *data, size_t size)
{
    AMCTRACE("%s", __func__);
    ssize_t write_ret = -1;
    jobject input_buffer_array = NULL;
    jobject input_buffer = NULL;

    JNIEnv *env = NULL;
    if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
        ALOGE("%s: SetupThreadEnv failed", __func__);
        return -1;
    }

    SDL_AMediaCodec_Opaque *opaque = (SDL_AMediaCodec_Opaque *)acodec->opaque;
    input_buffer_array = J4AC_MediaCodec__getInputBuffers__catchAll(env, opaque->android_media_codec);
    if (!input_buffer_array)
        return -1;

    int buffer_count = (*env)->GetArrayLength(env, input_buffer_array);
    if (J4A_ExceptionCheck__catchAll(env) || idx < 0 || idx >= buffer_count) {
        ALOGE("%s: idx(%d) < count(%d)\n", __func__, (int)idx, (int)buffer_count);
        goto fail;
    }

    input_buffer = (*env)->GetObjectArrayElement(env, input_buffer_array, idx);
    if (J4A_ExceptionCheck__catchAll(env) || !input_buffer) {
        ALOGE("%s: GetObjectArrayElement failed\n", __func__);
        goto fail;
    }

    {
        jlong buf_size = (*env)->GetDirectBufferCapacity(env, input_buffer);
        void *buf_ptr  = (*env)->GetDirectBufferAddress(env, input_buffer);

        // 此处因InputBuffe容量不足以容纳1个完整帧,而需多次提交的情况
        write_ret = size < buf_size ? size : buf_size;
        memcpy(buf_ptr, data, write_ret);
    }

fail:
    SDL_JNI_DeleteLocalRefP(env, &input_buffer);
    SDL_JNI_DeleteLocalRefP(env, &input_buffer_array);
    // 返回实际copy给mediacodec解码器InputBuffer的byte数
    return write_ret;
}

    d->pkt_temp偏移提交给mediacodec的长度,余下的数据降在下次_feed_input_buffer()时再次喂给解码器: 

    // SDL_AMediaCodec_queueInputBuffer()提交InputBuffer数据
    ......
    if (copy_size < 0) {
        d->packet_pending = 0;
    } else {
        d->pkt_temp.dts =
        d->pkt_temp.pts = AV_NOPTS_VALUE;
        if (d->pkt_temp.data) {
            // 1个AVPacket的数据,1个mediacodec的inputBuffer或装不下,需将剩下的数据装入下1个或下下1个inputBuffer中,直到装完
            d->pkt_temp.data += copy_size;
            d->pkt_temp.size -= copy_size;
            if (d->pkt_temp.size <= 0)
                d->packet_pending = 0;
        } else {
            // FIXME: detect if decode finished
            // if (!got_frame) {
                d->packet_pending = 0;
                // 到文件末,解码完毕
                d->finished = d->pkt_serial;
            // }
        }
    }
    ......
dequeInputBuffer异常

同步&异步

mediacodec扩展

NDK硬解

mediacodec硬解?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1572907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件服务器之二:SAMBA服务器

文章目录 什么是SAMBASAMBA的发展历史与名称的由来SAMBA常见的应用 SAMBA服务器基础配置配置共享资源Windows挂载共享Linux挂载共享 什么是SAMBA 下图来自百度百科 SAMBA的发展历史与名称的由来 Samba是一款开源的文件共享软件&#xff0c;它基于SMB&#xff08;Server Messa…

使用Element Plus

1. 官网安装 安装 | Element Plus (gitee.io) 安装&#xff1a; npm install element-plus --save 在main.ts中全局注册ElementPlus并使用 //加入element-plus import ElementPlus from element-plus; //加入element-plus样式 import element-plus/dist/index.css; import…

Day106:代码审计-PHP原生开发篇文件安全上传监控功能定位关键搜索1day挖掘

目录 emlog-文件上传&文件删除 emlog-模板文件上传 emlog-插件文件上传 emlog-任意文件删除 通达OA-文件上传&文件包含 知识点&#xff1a; PHP审计-原生开发-文件上传&文件删除-Emlog PHP审计-原生开发-文件上传&文件包含-通达OA emlog-文件上传&文件…

Unknown redis exception; event execu tor terminated;解决

最近查看服务器日记是不是报发现有台服务器报错&#xff1a; rocessing failed; nested exception is org.springframework.data.redis.RedisSystemException: Unknown redis exception; nested exception is java.util.concurrent.RejectedExecutionException: event execu …

Stale Diffusion、Drag Your Noise、PhysReaction、CityGaussian

本文首发于公众号&#xff1a;机器感知 Stale Diffusion、Drag Your Noise、PhysReaction、CityGaussian Drag Your Noise: Interactive Point-based Editing via Diffusion Semantic Propagation Point-based interactive editing serves as an essential tool to compleme…

Python实现特征模态分解(FMD)

大家好&#xff0c;我是带我去滑雪&#xff01; 特征模态分解&#xff08;Feature Mode Decomposition&#xff0c;FMD&#xff09;是一种信号处理技术&#xff0c;用于从数据中提取特征&#xff0c;并将其表示为一组特定的模态成分。与其他分解方法类似&#xff0c;如小波变换…

RUST语言值所有权之内存复制与移动

1.RUST中每个值都有一个所有者,每次只能有一个所有者 String::from函数会为字符串hello分配一块内存 内存示例如下: 在内存分配前调用s1正常输出 在分配s1给s2后调用报错 因为s1分配给s2后,s1的指向自动失效 s1被move到s2 s1自动释放 字符串克隆使用

Oracle 中 where 和 on 的区别

1.Oracle 中 where 和 on 的区别 on&#xff1a;会先根据on后面的条件进行筛选&#xff0c;条件为真时返回该行&#xff0c;由于on的优先级高于left join&#xff0c;所以left join关键字会把左表中没有匹配的所有行也都返回&#xff0c;然后生成临时表返回,执行优先级高于…

Python 基于列表实现的通讯录管理系统(有完整源码)

目录 通讯录管理系统 PersonInformation类 ContactList类 menu函数 main函数 程序的运行流程 完整代码 运行示例 通讯录管理系统 这是一个基于文本的界面程序&#xff0c;用户可以通过命令行与之交互&#xff0c;它使用了CSV文件来存储和读取联系人信息&#xff0c;这…

C++进阶--C++11(2)

C11第一篇 C11是C编程语言的一个版本&#xff0c;于2011年发布。C11引入了许多新特性&#xff0c;为C语言提供了更强大和更现代化的编程能力。 可变参数模板 在C11中&#xff0c;可变参数模板可以定义接受任意数量和类型参数的函数模板或类模板。它可以表示0到任意个数&…

数据库基础:概念、分类、作用和特点

文章目录 概要DB-Engines 排名数据库的分类数据库的作用数据库的特点数据库的应用小结 概要 数据库是按照数据结构来组织、存储和管理数据的仓库。它是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。数据库可以被视为电子化的文件柜&#xff0c;用…

40.基于SpringBoot + Vue实现的前后端分离-摄影分享网站(项目 + 论文)

项目介绍 随着互联网时代的发展&#xff0c;传统的线下管理技术已无法高效、便捷的管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;国家在环境要求不断提高的前提下&#xff0c;摄影分享网站管理系统建设也逐渐进入了信…

Lanelets_ 高效的自动驾驶地图表达方式

Lanelets: 高效的自动驾驶地图表达方式 附赠自动驾驶学习资料和量产经验&#xff1a;链接 LaneLets是自动驾驶领域高精度地图的一种高效表达方式&#xff0c;它以彼此相互连接的LaneLets来描述自动驾驶可行驶区域&#xff0c;不仅可以表达车道几何&#xff0c;也可以完整表述车…

考研高数(平面图形的面积,旋转体的体积)

1.平面图形的面积 纠正&#xff1a;参数方程求面积 2.旋转体的体积&#xff08;做题时&#xff0c;若以x为自变量不好计算&#xff0c;可以求反函数&#xff0c;y为自变量进行计算&#xff09;

正排索引 vs 倒排索引 - 搜索引擎具体原理

阅读导航 一、正排索引1. 概念2. 实例 二、倒排索引1. 概念2. 实例 三、正排 VS 倒排1. 正排索引优缺点2. 倒排索引优缺点3. 应用场景 三、搜索引擎原理1. 宏观原理2. 具体原理 一、正排索引 1. 概念 正排索引是一种索引机制&#xff0c;它将文档或数据记录按照某种特定的顺序…

【cpp】快速排序优化

标题&#xff1a;【cpp】快速排序 水墨不写bug 正文开始&#xff1a; 快速排序的局限性&#xff1a; 虽然快速排序是一种高效的排序算法&#xff0c;但也存在一些局限性&#xff1a; 最坏情况下的时间复杂度&#xff1a;如果选择的基准元素不合适&#xff0c;或者数组中存在大…

“张衡一号”卫星成功监测太阳活动引起的空间天气事件

太阳出现耀斑和日冕物质抛射等短时间尺度的剧烈活动&#xff0c;造成地球磁层、电离层和中高层大气的强烈扰动&#xff0c;这类活动通常称之为空间天气事件。空间天气事件会对现代高技术系统&#xff0c;如航空、航天、导航通信、电力油气管网等&#xff0c;造成严重影响&#…

Rust---复合数据类型之元组

目录 元组的使用输出结果 元组的使用 fn main() {// 创建一个元组let my_tuple : (i32, &str, f64) (10, "hello", 3.14);// 打印元组中的元素println!("{:?}", my_tuple);// 访问元组中的元素let first_element my_tuple.0; // 访问第一个元素let…

阿里云最新活动及优惠券领取指南

随着云计算技术的快速发展&#xff0c;越来越多的企业选择将业务部署在云平台上。阿里云作为国内领先的云服务提供商&#xff0c;不断推出各种优惠活动及优惠券&#xff0c;旨在帮助用户降低成本&#xff0c;提升运营效率。本文将为大家详细介绍阿里云的最新活动及优惠券领取指…

Web3 游戏周报(3.24-3.30)

【3.24-3.30】Web3 游戏行业动态&#xff1a; Web3 开发平台 Mirror World 在 Solana 上推出首个游戏 rollup 链 NFT 卡牌游戏 Parallel 完成 3,500 万美元融资&#xff0c;Solana Ventures 等参投 加密游戏开发公司 Gunzilla Games 完成 3,000 万美元融资 Telegram 游戏 No…