这一节是实践,学习android原生SimplePlayer并自己码一遍,果然学到很多。

MyPlayer.h

#include <media/stagefright/foundation/AHandler.h>
#include <media/stagefright/foundation/AString.h>
#include <utils/KeyedVector.h> namespace android { class IGraphicBufferProducer;
struct MediaCodec;
class MediaCodecBuffer;
struct NuMediaExtractor;
class Surface;
struct ABuffer;
class AudioTrack;
struct ALooper; class MyPlayer : public AHandler { public:
MyPlayer(); status_t setDataSource(const char* path);
status_t setSurface(const sp<IGraphicBufferProducer> &bufferProducer);
status_t prepare();
status_t start();
status_t stop(); protected:
virtual ~MyPlayer();
virtual void onMessageReceived(const sp<AMessage> &msg); private: enum {
kWhatSetDataSource = 0,
kWhatSetSurface,
kWhatPrepare,
kWhatStart,
kWhatStop,
kWhatDoMoreStuff,
}; enum State{
UNINITIALIZED,
UNPREPARED,
STOPPED,
STARTED,
}; struct BufferInfo{
size_t mIndex;
size_t mOffset;
size_t mSize;
int64_t mPresentationTimeUs;
uint32_t mFlags;
}; struct CodecState {
sp<MediaCodec> mCodec;
Vector<sp<MediaCodecBuffer>> mBuffers[2];
Vector<sp<ABuffer>> mCSD;
List<size_t> mAvailInputBufferIndices;
List<BufferInfo> mAvailOutputBufferInfos;
sp<AudioTrack> mAudioTrack;
uint32_t mNumFramesWritten;
}; status_t onPrepare();
status_t onStart();
status_t onStop();
status_t onDoMoreStuff();
status_t onOutputFormatChanged(CodecState *);
status_t renderAudio(CodecState* state, BufferInfo* info, const sp<MediaCodecBuffer> &buffer); AString mPath;
State mState;
sp<Surface> mSurface;
sp<NuMediaExtractor> mExtractor;
sp<ALooper> mCodecLooper;
KeyedVector<size_t, CodecState> mStateByTrackIndex;
int32_t mDoMoreStuffGeneration;
int64_t mStartTimeRealUs;
}; }

MyPlayer.cpp

//define LOG_NDEBUG 0
#define LOG_TAG "MyPlayer" #include "MyPlayer.h" #include <media/stagefright/foundation/ALooper.h>
#include <media/stagefright/foundation/AMessage.h>
#include <media/stagefright/foundation/ADebug.h>
#include <gui/Surface.h>
#include <media/stagefright/MediaCodec.h>
#include <media/stagefright/NuMediaExtractor.h>
#include <media/stagefright/foundation/ABuffer.h>
#include <media/MediaCodecBuffer.h>
#include <media/IMediaHTTPService.h>
#include <mediadrm/ICrypto.h>
#include <media/AudioTrack.h> namespace android { status_t PostAndAwaitResponse(const sp<AMessage> &msg, sp<AMessage> *response)
{
status_t err = msg->postAndAwaitResponse(response);
if(err != OK)
return err; if(!(*response)->findInt32("err", &err))
{
err = OK;
}
return err;
} MyPlayer::MyPlayer()
: mState(UNINITIALIZED),
mDoMoreStuffGeneration(0),
mStartTimeRealUs(-1ll)
{ } MyPlayer::~MyPlayer()
{ } status_t MyPlayer::setDataSource(const char* path)
{
sp<AMessage> msg = new AMessage(kWhatSetDataSource, this);
msg->setString("path", path);
sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
} status_t MyPlayer::setSurface(const sp<IGraphicBufferProducer> &BufferProducer)
{
sp<AMessage> msg = new AMessage(kWhatSetSurface, this); sp<Surface> surface;
if(BufferProducer != NULL)
surface = new Surface(BufferProducer); msg->setObject("surface", surface); sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
} status_t MyPlayer::prepare()
{
sp<AMessage> msg = new AMessage(kWhatPrepare, this);
sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
} status_t MyPlayer::start()
{
sp<AMessage> msg = new AMessage(kWhatStart, this);
sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
} status_t MyPlayer::stop()
{
sp<AMessage> msg = new AMessage(kWhatStop, this);
sp<AMessage> response;
return PostAndAwaitResponse(msg, &response);
} void MyPlayer::onMessageReceived(const sp<AMessage> &msg)
{
switch(msg->what())
{
case kWhatSetDataSource:
{
status_t err;
if(mState != UNINITIALIZED)
err = INVALID_OPERATION;
else {
CHECK(msg->findString("path", &mPath));
mState = UNPREPARED;
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
response->postReply(replyID);
break;
}
case kWhatSetSurface:
{
status_t err;
if(mState != UNPREPARED)
err = INVALID_OPERATION;
else{
sp<RefBase> obj;
CHECK(msg->findObject("surface", &obj));
mSurface = static_cast<Surface*>(obj.get());
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
response->postReply(replyID);
break;
}
case kWhatPrepare:
{
status_t err;
if(mState != UNPREPARED)
err = INVALID_OPERATION;
else{
err = onPrepare();
if(err == OK)
mState = STOPPED;
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
response->postReply(replyID);
break;
}
case kWhatStart:
{
status_t err = OK;
if(mState == UNPREPARED){
err = onPrepare();
if(err == OK)
mState = STOPPED;
} if(err == OK)
{
if(mState != STOPPED)
err = INVALID_OPERATION;
else{
err = onStart();
if(err == OK)
mState = STARTED;
}
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
response->postReply(replyID);
break;
}
case kWhatStop:
{
status_t err;
if(mState != STARTED)
err = INVALID_OPERATION;
else{
err = onStop();
if(err == OK)
mState = STOPPED;
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
sp<AMessage> response = new AMessage;
response->setInt32("err", err);
response->postReply(replyID);
break;
}
case kWhatDoMoreStuff:
{
int32_t generation;
CHECK(msg->findInt32("generation", &generation));
if(generation != mDoMoreStuffGeneration)
break; status_t err = onDoMoreStuff(); if(err == OK)
msg->post(10000ll); break;
} default:
break;
}
} status_t MyPlayer::onPrepare()
{
CHECK_EQ(mState, UNPREPARED); // 创建NuMediaExtractor
mExtractor = new NuMediaExtractor(); status_t err = mExtractor->setDataSource(NULL, mPath.c_str()); if(err != OK)
{
mExtractor.clear();
return err;
} if(mCodecLooper == NULL)
{
mCodecLooper = new ALooper;
mCodecLooper->start();
} bool haveAudio = false;
bool haveVideo = false;
for(size_t i = 0; i < mExtractor->countTracks(); i++)
{
sp<AMessage> format;
status_t err = mExtractor->getTrackFormat(i, &format);
CHECK_EQ(err, (status_t)OK); AString mime;
CHECK(format->findString("mime", &mime));
bool isVideo = !strncasecmp(mime.c_str(), "video/", 6);
if(!haveAudio && !strncasecmp(mime.c_str(), "audio/", 6))
haveAudio = true;
else if(!haveVideo && isVideo)
haveVideo = true;
else
continue; // 获取IMediaSource
err = mExtractor->selectTrack(i);
CHECK_EQ(err, (status_t)OK);
// 为audio 和 video track分别创建一个CodecState结构体,保存mediacodec相关内容
// 这里涉及到KeyedVector的用法,add方法的第一个参数为key,第二个参数为value,返回添加元素的索引
// editValueAt,查找指定索引对应的value
// keyAt,找到指定索引对应的key值
// editValueFor,查找指定key对应的value
CodecState* state = &mStateByTrackIndex.editValueAt(mStateByTrackIndex.add(i, CodecState()));
state->mCodec = MediaCodec::CreateByType(mCodecLooper, mime.c_str(), false);
state->mNumFramesWritten = 0; // 用于audio
CHECK(state->mCodec != NULL); // configure mediacodec
err = state->mCodec->configure(format, isVideo?mSurface:NULL, NULL, 0); CHECK_EQ(err, OK); size_t j = 0;
sp<ABuffer> buffer;
// 检查mediaformat中是否有csd buffer,可以使用AStringPrintf做字符串拼接
while(format->findBuffer(AStringPrintf("csd-%d", j).c_str(), &buffer)){
state->mCSD.push_back(buffer);
j++;
}
} for(size_t i = 0; i < mStateByTrackIndex.size(); i++)
{
ALOGD("mStateByTrackIndex[%d].key = %d", i, mStateByTrackIndex.keyAt(i)); } for(size_t i = 0; i < mStateByTrackIndex.size(); i++)
{
CodecState* state = &mStateByTrackIndex.editValueAt(i);
// 先启动mediacodec,启动之后才会完成buffer的分配工作
status_t err = state->mCodec->start();
CHECK_EQ(err, (status_t)OK); // 将两个track中mediacodec的input和output buffer都保存到CodecState中
err = state->mCodec->getInputBuffers(&state->mBuffers[0]);
CHECK_EQ(err, (status_t)OK); err = state->mCodec->getOutputBuffers(&state->mBuffers[1]);
CHECK_EQ(err, (status_t)OK); // 如果有csd buffer要先写给decoder
for(size_t j = 0; j < state->mCSD.size(); ++j)
{
const sp<ABuffer> &srcBuffer = state->mCSD.itemAt(j); size_t index;
// 阻塞等待获取一个inputbuffer index
err = state->mCodec->dequeueInputBuffer(&index, -1ll);
CHECK_EQ(err, (status_t)OK); const sp<MediaCodecBuffer> &dstBuffer = state->mBuffers[0].itemAt(index); // 检查dstbuffer的空间是否足够
// ABuffer的size方法返回真实数据长度,capactiy返回最大容量,offset返回起始读取位置
CHECK_LE(srcBuffer->size(), dstBuffer->capacity());
dstBuffer->setRange(0, srcBuffer->size());
memcpy(dstBuffer->data(), srcBuffer->data(), srcBuffer->size()); // 将buffer送给decoder,由于是CSD buffer,所以flag需要添加BUFFER_FLAG_CODECCONFIG
err = state->mCodec->queueInputBuffer(index, 0, dstBuffer->size(), 0ll, MediaCodec::BUFFER_FLAG_CODECCONFIG); CHECK_EQ(err, (status_t)OK);
} } return OK; } status_t MyPlayer::onStart()
{
CHECK_EQ(mState, STOPPED); // start之后会一直发送kWhatDoMoreStuff消息,不断执行onDoMoreStuff方法
sp<AMessage> msg = new AMessage(kWhatDoMoreStuff, this);
msg->setInt32("generation", ++mDoMoreStuffGeneration);
msg->post();
return OK;
} status_t MyPlayer::onStop()
{
return OK;
} status_t MyPlayer::onDoMoreStuff()
{
// 1、为每个track获取一个input 和一个output buffer
for(size_t i = 0; i < mStateByTrackIndex.size(); i++)
{
CodecState *state = &mStateByTrackIndex.editValueAt(i); status_t err; // 这里会不阻塞轮询,直到获取到一个input buffer
do{
size_t index;
err = state->mCodec->dequeueInputBuffer(&index);
if(err == OK)
{
// 将index加入到列表当中
state->mAvailInputBufferIndices.push_back(index);
}
else
{ }
}while(err == OK); // 同样是不阻塞轮询,直到获取到一个ouput buffer
do{
BufferInfo info;
// dequeueOutputBuffer拿到的不仅仅是index,还有buffer的offset、size,PTS,FLAGS等信息;
// dequeueOutputBuffer的返回值带有一些特殊信息,所以传回的buffer并不一定是解码结果,也可能是decoder送回的信息
err = state->mCodec->dequeueOutputBuffer(&info.mIndex, &info.mOffset, &info.mSize, &info.mPresentationTimeUs, &info.mFlags); if(err == OK)
{
// 如果是解码后的数据,将bufferInfo加入到列表等待处理
state->mAvailOutputBufferInfos.push_back(info);
}
else if(err == INFO_FORMAT_CHANGED)
{
// 如果返回值为INFO_FORMAT_CHANGED,说明视频格式发生变化
// 执行onOutPutFormatChanged方法中会创建AudioTrack
// 我觉得 AudioTrack也可以在MediaExtractor->getTrackFormat拿到audio track时创建
err = onOutputFormatChanged(state);
CHECK_EQ(err, (status_t)OK);
}
else if(err == INFO_OUTPUT_BUFFERS_CHANGED)
{
// 如果是INFO_OUTPUT_BUFFERS_CHANGED,说明用于输出的buffer发生变化,需要重新获取一次output buffer列表
err = state->mCodec->getOutputBuffers(&state->mBuffers[1]);
CHECK_EQ(err, (status_t)OK);
}
else
{ }
}while(err == OK || err == INFO_FORMAT_CHANGED || err == INFO_OUTPUT_BUFFERS_CHANGED);
} // 2、处理input buffer,这里不会向上面遍历所有track,这里的方法是按顺序读取文件,如果有某个track没有input buffer了就退出循环
for(;;)
{
size_t trackIndex;
// 获取当前数据所属的track index,返回值不等于OK说明到达了EOS
status_t err = mExtractor->getSampleTrackIndex(&trackIndex);
if(err != OK){
ALOGD("get input EOS");
break;
}
else{
// 拿到track对应的codecstate
CodecState *state = &mStateByTrackIndex.editValueFor(trackIndex);
// 如果当前CodecState中没有input buffer则直接退出循环
if(state->mAvailInputBufferIndices.empty())
break; // 取出一个index
size_t index = *state->mAvailInputBufferIndices.begin();
state->mAvailInputBufferIndices.erase(state->mAvailInputBufferIndices.begin()); // input和output buffer都是MediaCodecBuffer,base方法可以获取到存储数据的指针
const sp<MediaCodecBuffer> &dstBuffer = state->mBuffers[0].itemAt(index);
sp<ABuffer> abuffer = new ABuffer(dstBuffer->base(), dstBuffer->capacity()); // 将数据读到MediaCodecBuffer中
err = mExtractor->readSampleData(abuffer);
CHECK_EQ(err, (status_t)OK);
// 设置可读取范围
dstBuffer->setRange(abuffer->offset(), abuffer->size()); int64_t timeUs;
// 获取数据对应的PTS
CHECK_EQ(mExtractor->getSampleTime(&timeUs), OK); // 将数据送给decoder,同时附带PTS信息,不带FLAG
err = state->mCodec->queueInputBuffer(index, dstBuffer->offset(), dstBuffer->size(), timeUs, 0);
CHECK_EQ(err, (status_t)OK); ALOGD("enqueue input data on track %u", trackIndex); // 将Extractor指向下一笔数据
err = mExtractor->advance();
CHECK_EQ(err, (status_t)OK);
}
} int64_t nowUs = ALooper::GetNowUs();
// 初始化开始播放时间
if(mStartTimeRealUs < 0ll)
{
mStartTimeRealUs = nowUs + 1000000ll;
} // 3、处理output buffer并做av sync,遍历所有track
for(size_t i = 0; i < mStateByTrackIndex.size(); i++)
{
CodecState *state = &mStateByTrackIndex.editValueAt(i); // 一次性处理所有的outputbuffer
while(!state->mAvailOutputBufferInfos.empty()){
BufferInfo *info = &*state->mAvailOutputBufferInfos.begin(); // 计算真实播放时间
int64_t whenRealUs = info->mPresentationTimeUs + mStartTimeRealUs;
// 计算真实播放时间和当前系统时间的距离
int64_t lateByUs = nowUs - whenRealUs; // 如果系统时间距离播放时间小于10ms,则去渲染
if(lateByUs > -10000ll)
{
bool release = true; // 如果系统时间已经过了播放时间30ms则丢弃帧
if(lateByUs > 30000ll)
{
ALOGD("track %u, buffer late by %lld us, dropping", mStateByTrackIndex.keyAt(i),lateByUs);
// 直接调用release
state->mCodec->releaseOutputBuffer(info->mIndex);
}
else{
// 用于audio
if(state->mAudioTrack != NULL)
{
// 获取到output buffer
const sp<MediaCodecBuffer> &srcBuffer = state->mBuffers[1].itemAt(info->mIndex);
// 将数据写到audioTrack中,但是有可能一次写不完
renderAudio(state, info, srcBuffer);
// 根据mSize判断是否写完,写完了才回去release
if(info->mSize > 0)
release = false; }
// render,由于是等到绘制时间才去release and render,所以不用指定PTS
if(release)
state->mCodec->renderOutputBufferAndRelease(info->mIndex);
}
// 从列表中删除
if(release){
state->mAvailOutputBufferInfos.erase(state->mAvailOutputBufferInfos.begin());
info = NULL;
}
else
break;
}else {
// 如果系统时间早于播放时间10ms,则不去渲染
ALOGD("track %u buffer early by %lld us", mStateByTrackIndex.keyAt(i), lateByUs);
break;
}
}
} return OK;
} status_t MyPlayer::onOutputFormatChanged(CodecState *state)
{
sp<AMessage> format;
// getOutputFormat可以获取decoder返回的format
status_t err = state->mCodec->getOutputFormat(&format);
if(err != OK)
return err; AString mime;
CHECK(format->findString("mime", &mime));
if(!strncasecmp(mime.c_str(), "audio/", 6))
{
int32_t channelCount;
int32_t sampleRate = 0;
// 获取通道数和采样率
CHECK(format->findInt32("channel-count", &channelCount));
CHECK(format->findInt32("sample-rate", &sampleRate));
// 创建一个AudioTrack
state->mAudioTrack = new AudioTrack(AUDIO_STREAM_MUSIC, sampleRate, AUDIO_FORMAT_PCM_16_BIT, audio_channel_out_mask_from_count(channelCount), 0);
} return OK;
} status_t MyPlayer::renderAudio(CodecState* state, BufferInfo* info, const sp<MediaCodecBuffer> &buffer)
{
CHECK(state->mAudioTrack != NULL);
// 开启audio render
if(state->mAudioTrack->stopped())
state->mAudioTrack->start(); uint32_t numFramesPlayed;
// 获取当前已经播放的帧数
CHECK_EQ(state->mAudioTrack->getPosition(&numFramesPlayed), OK); // 获取可以写入的帧数
uint32_t numFramesAvailableToWrite = state->mAudioTrack->frameCount() - (state->mNumFramesWritten - numFramesPlayed); // 计算可写入数据大小
size_t numBytesAvailableToWrite = numFramesAvailableToWrite * state->mAudioTrack->frameSize(); size_t copy = info->mSize;
if(copy > numBytesAvailableToWrite)
copy = numBytesAvailableToWrite; if(copy == 0)
return OK; int64_t startTimeUs = ALooper::GetNowUs(); // 写入数据,注意这里有偏移量offset(上次可能没写完)
ssize_t nbytes = state->mAudioTrack->write(buffer->base() + info->mOffset, copy); CHECK_EQ(nbytes, (ssize_t)copy); int64_t delayUs = ALooper::GetNowUs() - startTimeUs; // 计算写入帧数
uint32_t numFramesWritten = nbytes / state->mAudioTrack->frameSize(); if(delayUs > 2000ll){
ALOGD("AudioTrack write took %lld us", delayUs);
} // 将offset和size记录并返回,用于判断数据是否全部写入
info->mOffset += nbytes;
info->mSize -= nbytes; // 记录已经写入帧数
state->mNumFramesWritten += numFramesWritten;
return OK;
}
}

TestPlayer.cpp

//#define LOG_NDEBUG 0
#define LOG_TAG "TestPlayer" #include "MyPlayer.h"
#include <gui/SurfaceControl.h>
#include <gui/SurfaceComposerClient.h>
#include <gui/Surface.h>
#include <ui/DisplayConfig.h>
#include <media/stagefright/foundation/ALooper.h>
#include <binder/IBinder.h>
#include <media/stagefright/foundation/ADebug.h> int main(int argc, char** argv)
{
using namespace android; sp<MyPlayer> player = new MyPlayer();
sp<android::ALooper> looper = new android::ALooper();
// 创建looper处理MyPlayer的消息
looper->registerHandler(player);
// 运行在新的线程上
looper->start(); // 创建Surface
sp<SurfaceComposerClient> composerClient = new SurfaceComposerClient;
CHECK_EQ(composerClient->initCheck(), (status_t)OK); const sp<IBinder> display = SurfaceComposerClient::getInternalDisplayToken();
CHECK(display != NULL); DisplayConfig config;
CHECK_EQ(SurfaceComposerClient::getActiveDisplayConfig(display, &config), NO_ERROR); const ui::Size &resolution = config.resolution;
const ssize_t displayWidth = resolution.getWidth();
const ssize_t displayHeight = resolution.getHeight();
ALOGD("display is %d x %d\n", displayWidth, displayHeight); sp<SurfaceControl> control = composerClient->createSurface(String8("A Surface"), displayWidth/2, displayHeight/2, PIXEL_FORMAT_RGB_565, 0);
CHECK(control != NULL);
CHECK(control->isValid()); SurfaceComposerClient::Transaction{}.setLayer(control, INT_MAX).show(control).apply();
sp<Surface> surface = control->getSurface();
CHECK(surface != NULL); // 开始播放
player->setDataSource(argv[1]);
player->setSurface(surface->getIGraphicBufferProducer());
player->start(); // sleep 60s,等待播放60s
sleep(60); composerClient->dispose(); looper->stop(); return 0; }

Android.bp

cc_binary {
name: "MyPlayer", srcs: [
"TestPlayer.cpp",
"MyPlayer.cpp",
], local_include_dirs: [
"include"
], shared_libs: [
"libstagefright",
"libmedia",
"libstagefright_foundation",
"libgui",
"libaudioclient",
"liblog",
"libutils",
"libcutils",
"libmedia_omx",
"libui",
], /*export_include_dirs: [
"include"
],*/ export_shared_lib_headers:[
"libstagefright",
"libmedia",
], header_libs: [
"libmediametrics_headers",
"libmediadrm_headers",
"libaudioclient_headers"
], }

记录一下编译过程中发现的问题:

1、local_include_dirs 和 export_include_dirs都是列表,所以要用 [] 将引用的内容框起来

2、cc_binary中不允许出现export_include_dirs,因为不需要导出头文件

3、main函数不能被包在 namespace android当中,不然会找不到main函数,可以在main函数中加上using namespace android,ALooper仍然需要命名空间、

4、还有以class形式替代头文件可能会出现的错误

Android 11(R) MultiMedia(十五)MediaCodec同步模式实现一个简易播放器的更多相关文章

  1. Android 11(R) Power HAL AIDL简析 -- 基本接口

    Android 11(R) Power HAL AIDL将分三篇文章来介绍: Android 11(R) Power HAL AIDL简析 -- 基本接口 Android 11(R) Power HA ...

  2. 【转】设计模式 ( 十五 ) 中介者模式Mediator(对象行为型)

    设计模式 ( 十五 ) 中介者模式Mediator(对象行为型) 1.概述 在面向对象的软件设计与开发过程中,根据"单一职责原则",我们应该尽量将对象细化,使其只负责或呈现单一的职 ...

  3. 设计模式 ( 十五 ) 中介者模式Mediator(对象行为型)

    设计模式 ( 十五 ) 中介者模式Mediator(对象行为型) 1.概述 在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责,即将行为分布到各 ...

  4. webpack4 系列教程(十五):开发模式与webpack-dev-server

    作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步<webpack4 系列教程(十五):开发模式与 webpack-dev-server>原文地址.更欢迎来我的 ...

  5. Java 设计模式系列(十五)迭代器模式(Iterator)

    Java 设计模式系列(十五)迭代器模式(Iterator) 迭代器模式又叫游标(Cursor)模式,是对象的行为模式.迭代子模式可以顺序地访问一个聚集中的元素而不必暴露聚集的内部表象(interna ...

  6. FFmpeg简易播放器的实现-音视频同步

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10284653.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  7. Android学习笔记(十二)——实战:制作一个聊天界面

    //此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 运用简单的布局知识,我们可以来尝试制作一个聊天界面. 一.制作 Nine-Patch 图片 : Nine-Pa ...

  8. 从零开始学习PYTHON3讲义(十四)写一个mp3播放器

    <从零开始PYTHON3>第十四讲 通常来说,Python解释执行,运行速度慢,并不适合完整的开发游戏.随着电脑速度的快速提高,这种情况有所好转,但开发游戏仍然不是Python的重点工作. ...

  9. Android学习笔记(十五)——实战:强制下线

    //此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 实现强制下线功能的思路也比较简单,只需要在界面上弹出一个对话框, 让用户无法进行任何其他操作, 必须要点击对话 ...

  10. Android核心分析之十五Android输入系统之输入路径详解

       Android用户事件输入路径 1 输入路径的一般原理 按键,鼠标消息从收集到最终将发送到焦点窗口,要经历怎样的路径,是Android GWES设计方案中需要详细考虑的问题.按键,鼠标等用户消息 ...

随机推荐

  1. HDD杭州站•ArkUI让开发更灵活

    原文:https://mp.weixin.qq.com/s/cX48CPs61daKOC2J5znyJw,点击链接查看更多技术内容. 7月15日的HUAWEI Developer Day(简称HDD) ...

  2. 分布式文件存储-FastDFS

    1.1 FastDFS简介 1.1.1 FastDFS体系结构 FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储.文件同步.文件访问(文件上传.文件下载)等,解决了 ...

  3. Linux系统Mariadb初始化相关(ubuntu)

    #事先声明,此文是一边写一边操作的,中间可能有不一致的地方,大体思路就是参照windows下的目录规范,将 mysql的各目录及文件进行类比放置,然后执行重建数据库命令,也许你只是想修改下data目录 ...

  4. HarmonyOS NEXT应用开发——Navigation开发 页面切换场景范例

    简介 在应用开发时,我们常常遇到,需要在应用内多页面跳转场景时中使用Navigation导航组件做统一的页面跳转管理,它提供了一系列属性方法来设置页面的标题栏.工具栏以及菜单栏的各种展示样式.除此之外 ...

  5. 技术干货 | 深度解构 Android 应用面临紧急发版时的救星方案:mPaaS 热修复——DexPatch

    简介: 关于 Android 热修复方案--DexPatch 的介绍与使用说明 方案介绍 为了解决 Native 模块上线后的问题,mPaaS 提供了热修复功能,实现不发布客户端 apk 场景下的热修 ...

  6. 基于 Wasm 和 ORAS 简化扩展服务网格功能

    简介: 本文将介绍如何使用 ORAS 客户端将具有允许的媒体类型的 Wasm 模块推送到 ACR 注册库(一个 OCI 兼容的注册库)中,然后通过 ASM 控制器将 Wasm Filter 部署到指定 ...

  7. dotnet OpenXML 文本删除线解析方法

    本文来告诉大家如何解析读取在 OpenXML 里面存放的文本删除线,本文使用 PowerPoint 作为例子来告诉大家如何读取然后在 WPF 应用里面显示 在开始之前,期望大家已了解如何在 dotne ...

  8. Docker镜像基本原理

    前言 Docker系列文章: 如果没有安装过Docker请参考本文最后部分,大家从现在开始一定要按照我做的Demo都手敲一遍,印象会更加深刻的,加油! 为什么学习Docker Docker基本概念 什 ...

  9. 一文搞懂Maven配置,从此不再糊涂下载依赖(文末有成品)

    一般来说Maven都是配合着idea一起使用,下载依赖速度慢就去网上找个镜像配置一下,但总会遇到莫名其妙的问题,比如镜像源不生效.Error reading file pom.xml等等.今天详细讲解 ...

  10. 一分钟部署 Llama3 中文大模型,没别的,就是快

    前段时间百度创始人李彦宏信誓旦旦地说开源大模型会越来越落后,闭源模型会持续领先.随后小扎同学就给了他当头一棒,向他展示了什么叫做顶级开源大模型. 美国当地时间4月18日,Meta 在官网上发布了两款开 ...