巴延興
深圳開(kāi)鴻數(shù)字產(chǎn)業(yè)發(fā)展有限公司
資深OS框架開(kāi)發(fā)工程師
一、 簡(jiǎn)介
多媒體播放框架主要的實(shí)現(xiàn)在PlayerServer服務(wù)中,這個(gè)服務(wù)提供了媒體播放框架所需要的實(shí)現(xiàn)環(huán)境,繼續(xù)跟蹤代碼分析發(fā)現(xiàn),PlayerServer主要通過(guò)gstreamer適配層,對(duì)gstreamer進(jìn)行調(diào)用。gstreamer屬于更加具體的實(shí)現(xiàn),所以本篇文章主要是分析PlayerServer通過(guò)適配層調(diào)用到gstreamer的過(guò)程。
此前,我在《OpenHarmony 3.2 Beta多媒體系列-音視頻播放框架》一文中,主要分析了多媒體播放的框架層代碼,本地接口通過(guò)服務(wù)端的proxy代理類(lèi)進(jìn)行IPC調(diào)用,最終調(diào)用到PlayerServer服務(wù)端。本篇主要分析了多媒體gstreamer的調(diào)用,涉及到從PlayerServer到gstreamer的整體流程。
二、 目錄
gstreamer
├── BUILD.gn
├── common
│ ├── BUILD.gn
│ ├── playbin_adapter
│ │ ├── i_playbin_ctrler.h
│ │ ├── playbin2_ctrler.cpp
│ │ ├── playbin2_ctrler.h
│ │ ├── playbin_ctrler_base.cpp
│ │ ├── playbin_ctrler_base.h
│ │ ├── playbin_msg_define.h
│ │ ├── playbin_sink_provider.h
│ │ ├── playbin_state.cpp
│ │ ├── playbin_state.h
│ │ ├── playbin_task_mgr.cpp
│ │ └── playbin_task_mgr.h
│ ├── state_machine
│ │ ├── state_machine.cpp
│ │ └── state_machine.h
├── factory
│ ├── BUILD.gn
│ └── engine_factory.cpp
└── player
├── BUILD.gn
├── player_codec_ctrl.cpp
├── player_codec_ctrl.h
├── player_engine_gst_impl.cpp
├── player_engine_gst_impl.h
├── player_sinkprovider.cpp
├── player_sinkprovider.h
├── player_track_parse.cpp
└── player_track_parse.h
目錄主要是多媒體子系統(tǒng)中的engine部分,涉及到了gstreamer的適配層,gstreamer具體的實(shí)現(xiàn)是在third_party/gstreamer目錄中。
三 、Gstreamer介紹
1. 簡(jiǎn)介
Gstreamer是一個(gè)跨平臺(tái)的多媒體框架,應(yīng)用程序可以通過(guò)管道(Pipeline)的方式,將多媒體處理的各個(gè)步驟串聯(lián)起來(lái),達(dá)到預(yù)期的效果。每個(gè)步驟通過(guò)元素(Element)基于GObject對(duì)象系統(tǒng)通過(guò)插件(plugins)的方式實(shí)現(xiàn),方便了各項(xiàng)功能的擴(kuò)展。
2.Gstreamer幾個(gè)重要的概念
Element
Element是Gstreamer中最重要的對(duì)象類(lèi)型之一。一個(gè)element實(shí)現(xiàn)一個(gè)功能(讀取文件,解碼,輸出等),程序需要?jiǎng)?chuàng)建多個(gè)element,并按順序?qū)⑵浯?lián)起來(lái),構(gòu)成一個(gè)完整的Pipeline。
Pad
Pad是一個(gè)element的輸入/輸出接口,分為src pad(生產(chǎn)數(shù)據(jù))和sink pad(消費(fèi)數(shù)據(jù))兩種。兩個(gè)element必須通過(guò)pad才能連接起來(lái),pad擁有當(dāng)前element能處理數(shù)據(jù)類(lèi)型的能力(capabilities),會(huì)在連接時(shí)通過(guò)比較src pad和sink pad中所支持的能力,來(lái)選擇最恰當(dāng)?shù)臄?shù)據(jù)類(lèi)型用于傳輸,如果element不支持,程序會(huì)直接退出。在element通過(guò)pad連接成功后,數(shù)據(jù)會(huì)從上一個(gè)element的src pad傳到下一個(gè)element的sink pad然后進(jìn)行處理。
Bin和Pipeline
Bin是一個(gè)容器,用于管理多個(gè)element,改變bin的狀態(tài)時(shí),bin會(huì)自動(dòng)去修改所包含的element的狀態(tài),也會(huì)轉(zhuǎn)發(fā)所收到的消息。如果沒(méi)有bin,我們需要依次操作我們所使用的element。通過(guò)bin降低了應(yīng)用的復(fù)雜度。
Pipeline繼承自bin,為程序提供一個(gè)bus用于傳輸消息,并且對(duì)所有子element進(jìn)行同步。當(dāng)將Pipeline的狀態(tài)設(shè)置為PLAYING時(shí),Pipeline會(huì)在一個(gè)/多個(gè)新的線程中通過(guò)element處理數(shù)據(jù)。
四、調(diào)用流程
?
左右滑動(dòng)查看更多
五、源碼分析
1. PrepareAsync分析
首先,在PlayerServer的PrepareAsync中會(huì)調(diào)用OnPrepare(false),具體是在OnPrepare(false)中實(shí)現(xiàn),參數(shù)傳入false,表明調(diào)用的是異步方法。
int32_tPlayerServer::PrepareAsync()
{
std::lock_guard<std::mutex> lock(mutex_);
MEDIA_LOGW("KPI-TRACE: PlayerServer PrepareAsync in");
if (lastOpStatus_ == PLAYER_INITIALIZED || lastOpStatus_ == PLAYER_STOPPED) {
return OnPrepare(false);
} else {
MEDIA_LOGE("Can not Prepare, currentState is %{public}s", GetStatusDescription(lastOpStatus_).c_str());
return MSERR_INVALID_OPERATION;
}
}
OnPrepare方法中,先通過(guò)playerEngine_調(diào)用SerVideoSurface的方法,將surface_設(shè)置到PlayerEngineGstImpl中(producerSurface_),接著啟動(dòng)一個(gè)任務(wù),調(diào)用目前狀態(tài)的Prepare()方法。
int32_tPlayerServer::OnPrepare(boolsync)
{
CHECK_AND_RETURN_RET_LOG(playerEngine_ != nullptr, MSERR_NO_MEMORY, "playerEngine_ is nullptr");
int32_t ret = MSERR_OK;
if (surface_ != nullptr) {
ret = playerEngine_->SetVideoSurface(surface_);
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, MSERR_INVALID_OPERATION, "Engine SetVideoSurface Failed!");
}
lastOpStatus_ = PLAYER_PREPARED;
auto preparedTask = std::make_sharedint32_t<>>([this]() {
MediaTrace::TraceBegin("PlayerServer::PrepareAsync", FAKE_POINTER(this));
auto currState = std::static_pointer_cast(GetCurrState());
return currState->Prepare();
});
ret = taskMgr_.LaunchTask(preparedTask, PlayerServerTaskType::STATE_CHANGE);
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, ret, "Prepare launch task failed");
if (sync) {
(void)preparedTask->GetResult(); // wait HandlePrpare
}
return MSERR_OK;
}
進(jìn)入Preparing狀態(tài)后,會(huì)觸發(fā)PlayerServer的HandlePrepare()方法被調(diào)用,在這個(gè)方法里會(huì)通過(guò)playerEngine_調(diào)用PrepareAsync方法,這個(gè)方法調(diào)用的是PlayerEngineGstImpl對(duì)應(yīng)的PrepareAsync方法。
int32_tPlayerServer::HandlePrepare()
{
int32_t ret = playerEngine_->PrepareAsync();
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, MSERR_INVALID_OPERATION, "Server Prepare Failed!");
if (config_.leftVolume <= 1.0f || config_.rightVolume <= 1.0f) {
ret = playerEngine_->SetVolume(config_.leftVolume, config_.rightVolume);
MEDIA_LOGD("Prepared SetVolume leftVolume:%{public}f rightVolume:%{public}f, ret:%{public}d",
config_.leftVolume, config_.rightVolume, ret);
}
(void)playerEngine_->SetLooping(config_.looping);
{
auto rateTask = std::make_sharedvoid<>>([this]() {
auto currState = std::static_pointer_cast(GetCurrState());
(void)currState->SetPlaybackSpeed(config_.speedMode);
});
(void)taskMgr_.LaunchTask(rateTask, PlayerServerTaskType::RATE_CHANGE);
}
return MSERR_OK;
}
首先初始化playBinCtrler_,后續(xù)的操作都是通過(guò)PlayBinCtrlerBase對(duì)象來(lái)操作的,所以PlayBinCtrlerInit()方法會(huì)創(chuàng)建PlayBinCtrlerBase對(duì)象(playBinCtrler_),創(chuàng)建好以后通過(guò)playBinCtrler_進(jìn)行SetSource和SetXXXListener的設(shè)置。
int32_tPlayerEngineGstImpl::PrepareAsync()
{
std::unique_lock<std::mutex> lock(mutex_);
MEDIA_LOGD("Prepare in");
int32_t ret = PlayBinCtrlerInit();
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, MSERR_INVALID_VAL, "PlayBinCtrlerInit failed");
CHECK_AND_RETURN_RET_LOG(playBinCtrler_ != nullptr, MSERR_INVALID_VAL, "playBinCtrler_ is nullptr");
ret = playBinCtrler_->PrepareAsync();
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, ret, "PrepareAsync failed");
// The duration of some resources without header information cannot be obtained.
MEDIA_LOGD("Prepared ok out");
return MSERR_OK;
}
初始化完成以后,接下來(lái)進(jìn)行playBinCtrler_的PrepareAsync的調(diào)用,PlayBinCtrlerBase中的PrepareAsync的方法間接地調(diào)用了PrepareAsyncInternal。
int32_tPlayBinCtrlerBase::PrepareAsync()
{
MEDIA_LOGD("enter");
std::unique_lock<std::mutex> lock(mutex_);
return PrepareAsyncInternal();
}
PrepareAsyncInternal首先判斷當(dāng)前的狀態(tài),如果是preparingState或preparedState,那么就直接返回成功,否則繼續(xù)向下調(diào)用。接下來(lái)會(huì)調(diào)用EnterInitializedState(),這個(gè)方法中會(huì)創(chuàng)建playbin,設(shè)置signal的回調(diào)以及gstreamer參數(shù)的設(shè)置。最后調(diào)用目前狀態(tài)的Prepare方法,此時(shí)的狀態(tài)是InitializedState。
int32_t PlayBinCtrlerBase::PrepareAsyncInternal()
{
if ((GetCurrState() == preparingState_) || (GetCurrState() == preparedState_)) {
MEDIA_LOGI("already at preparing state, skip");
return MSERR_OK;
}
CHECK_AND_RETURN_RET_LOG((!uri_.empty() || appsrcWrap_), MSERR_INVALID_OPERATION, "Set uri firsty!");
int32_t ret = EnterInitializedState();
CHECK_AND_RETURN_RET(ret == MSERR_OK, ret);
auto currState = std::static_pointer_cast(GetCurrState());
ret = currState->Prepare();
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, ret, "PrepareAsyncInternal failed");
return MSERR_OK;
}
InitializedState的Prepare方法又通過(guò)ctrler_調(diào)回到PlayBinCtrlerBase的ChangeState方法,這個(gè)方法是在PlayBinCtrlerBase的父類(lèi)StateMachine中,它是一個(gè)狀態(tài)機(jī),管理著各種狀態(tài)的切換。
int32_t PlayBinCtrlerBase::Prepare()
{
ctrler_.ChangeState(ctrler_.preparingState_);
return MSERR_OK;
}
很多表示狀態(tài)的類(lèi)在PlayBinCtrlerBase中進(jìn)行聲明,這些子類(lèi)的具體實(shí)現(xiàn)功能在playbin_state.cpp中。
private:
class BaseState;
class IdleState;
class InitializedState;
class PreparingState;
class PreparedState;
class PlayingState;
class PausedState;
class StoppedState;
class StoppingState;
class PlaybackCompletedState;
接下來(lái)看一下?tīng)顟B(tài)機(jī)的ChangeState方法,可以看出切換狀態(tài)的時(shí)候,先調(diào)用切換前狀態(tài)的StateExit()方法,再調(diào)用切換后狀態(tài)的StateEnter()。如果需要一些操作,我們可以在狀態(tài)的StateEnter和StateExit中進(jìn)行。
voidStateMachine::ChangeState(conststd::shared_ptr&state)
{
......
if (currState_ != nullptr && currState_->GetStateName() == "stopping_state" && state->GetStateName() != "stopped_state") {
return;
}
if (currState_) {
currState_->StateExit();
}
currState_ = state;
state->StateEnter();
}
因?yàn)樯厦媲袚Q狀態(tài)調(diào)用的是ctrler_.ChangeState(ctrler_.preparingState_),所以接下來(lái)看一下PreparingState狀態(tài)的StateEnter方法。這個(gè)方法中首先是調(diào)用了ctrler_.ReportMessage(msg),字面上看是用來(lái)上報(bào)msg信息的。
voidPlayBinCtrlerBase::StateEnter()
{
PlayBinMessage msg = { PLAYBIN_MSG_SUBTYPE, PLAYBIN_SUB_MSG_BUFFERING_START, 0, {} };
ctrler_.ReportMessage(msg);
GstStateChangeReturn ret;
(void)ChangePlayBinState(GST_STATE_PAUSED, ret);
MEDIA_LOGD("PreparingState::StateEnter finished");
}
ctrler_是PlayBinCtrlerBase類(lèi)型的變量,直接看PlayBinCtrlerBase的ReportMessage方法,這個(gè)方法的核心,是創(chuàng)建一個(gè)任務(wù)后,將任務(wù)放入消息隊(duì)列中,等待消息被處理,這里我們最想知道的是這個(gè)消息會(huì)在什么地方被處理。msgReportHandler創(chuàng)建了TaskHandler,這個(gè)里面會(huì)調(diào)用notifier_(msg),這里的notifier_比較重要,我們可以順著這個(gè)變量向上分析。
voidPlayBinCtrlerBase::ReportMessage(constPlayBinMessage&msg)
{
......
auto msgReportHandler = std::make_sharedvoid<>>([this, msg]() { notifier_(msg); });
int32_t ret = msgQueue_->EnqueueTask(msgReportHandler);
if (ret != MSERR_OK) {
MEDIA_LOGE("async report msg failed, type: %{public}d, subType: %{public}d, code: %{public}d",
msg.type, msg.subType, msg.code);
};
if (msg.type == PlayBinMsgType::PLAYBIN_MSG_EOS) {
ProcessEndOfStream();
}
}
notifier_是在PlayBinCtrlerBase被創(chuàng)建的時(shí)候賦值的。
PlayBinCtrlerBase::PlayBinCtrlerBase(constPlayBinCreateParam&createParam)
: renderMode_(createParam.renderMode),
notifier_(createParam.notifier),
sinkProvider_(createParam.sinkProvider)
{
MEDIA_LOGD("enter ctor, instance: 0x%{public}06" PRIXPTR "", FAKE_POINTER(this));
}
在源碼分析的前期PlayerEngineGstImpl初始化PlayBinCtrlerBase的時(shí)候進(jìn)行了創(chuàng)建notifier = std::bind(&PlayerEngineGstImpl::OnNotifyMessage, this, std::_1) notifier相當(dāng)于是調(diào)用了PlayerEngineGstImpl::OnNotifyMessage方法。所以上述中的處理函數(shù)就是PlayerEngineGstImpl::OnNotifyMessage。
int32_tPlayerEngineGstImpl::PlayBinCtrlerPrepare()
{
uint8_t renderMode = IPlayBinCtrler::DEFAULT_RENDER;
auto notifier = std::bind(&PlayerEngineGstImpl::OnNotifyMessage, this, std::_1);
{
std::unique_lock<std::mutex> lk(trackParseMutex_);
sinkProvider_ = std::make_shared(producerSurface_);
sinkProvider_->SetAppInfo(appuid_, apppid_);
}
IPlayBinCtrler::PlayBinCreateParam createParam = {
static_cast(renderMode), notifier, sinkProvider_
};
playBinCtrler_ = IPlayBinCtrler::PLAYBIN2, createParam);
......
return MSERR_OK;
}
在OnNotifyMessage中指定了各種消息類(lèi)型對(duì)應(yīng)的執(zhí)行函數(shù),上述代碼中創(chuàng)建的Message類(lèi)型是PLAYBIN_MSG_SUBTYPE,子類(lèi)型為PLAYBIN_SUB_MSG_BUFFERING_START。
voidPlayerEngineGstImpl::OnNotifyMessage(constPlayBinMessage&msg)
{
const std::unordered_map MSG_NOTIFY_FUNC_TABLE = {,>
{ PLAYBIN_MSG_ERROR, std::bind(&PlayerEngineGstImpl::HandleErrorMessage, this, std::_1) },
{ PLAYBIN_MSG_SEEKDONE, std::bind(&PlayerEngineGstImpl::HandleSeekDoneMessage, this, std::_1) },
{ PLAYBIN_MSG_SPEEDDONE, std::bind(&PlayerEngineGstImpl::HandleInfoMessage, this, std::_1) },
{ PLAYBIN_MSG_BITRATEDONE, std::bind(&PlayerEngineGstImpl::HandleInfoMessage, this, std::_1)},
{ PLAYBIN_MSG_EOS, std::bind(&PlayerEngineGstImpl::HandleInfoMessage, this, std::_1) },
{ PLAYBIN_MSG_STATE_CHANGE, std::bind(&PlayerEngineGstImpl::HandleInfoMessage, this, std::_1) },
{ PLAYBIN_MSG_SUBTYPE, std::bind(&PlayerEngineGstImpl::HandleSubTypeMessage, this, std::_1) },
{ PLAYBIN_MSG_AUDIO_SINK, std::bind(&PlayerEngineGstImpl::HandleAudioMessage, this, std::_1) },
{ PLAYBIN_MSG_POSITION_UPDATE, std::bind(&PlayerEngineGstImpl::HandlePositionUpdateMessage, this,
std::_1) },
};
if (MSG_NOTIFY_FUNC_TABLE.count(msg.type) != 0) {
MSG_NOTIFY_FUNC_TABLE.at(msg.type)(msg);
}
}
最終的流程走到了PlayerEngineGstImpl::HandleBufferingStart(),在這個(gè)方法中,主要通過(guò)obs_將format傳給IPlayerEngineObs的OnInfo方法。
voidPlayerEngineGstImpl::HandleBufferingStart()
{
percent_ = 0;
Format format;
(void)format.PutIntValue(std::string(PlayerKeys::PLAYER_BUFFERING_START), 0);
std::shared_ptr notifyObs = obs_.lock();
if (notifyObs != nullptr) {
notifyObs->OnInfo(INFO_TYPE_BUFFERING_UPDATE, 0, format);
}
}
我們重點(diǎn)看一下obs_是哪里設(shè)置的,在PlayerServer的初始化InitPlayEngine。shared_from_this()相當(dāng)于是把PlayerServer自身賦值給obs,PlayerServer也是實(shí)現(xiàn)了IPlayerEngineObs對(duì)應(yīng)的接口。
int32_tPlayerServer::InitPlayEngine(conststd::string&url)
{
......
int32_t ret = taskMgr_.Init();
auto engineFactory = EngineFactoryRepo::SCENE_PLAYBACK, url);
playerEngine_ = engineFactory->CreatePlayerEngine(appUid_, appPid_);
if (dataSrc_ == nullptr) {
ret = playerEngine_->SetSource(url);
} else {
ret = playerEngine_->SetSource(dataSrc_);
}
std::shared_ptr obs = shared_from_this();
ret = playerEngine_->SetObs(obs);
lastOpStatus_ = PLAYER_INITIALIZED;
ChangeState(initializedState_);
return MSERR_OK;
}
這樣我們就跟蹤到了PlayerServer的OnInfo()方法。
voidPlayerServer::OnInfo(PlayerOnInfoTypetype,int32_textra,constFormat&infoBody)
{
std::lock_guard<std::mutex> lockCb(mutexCb_);
int32_t ret = HandleMessage(type, extra, infoBody);
if (playerCb_ != nullptr && ret == MSERR_OK) {
playerCb_->OnInfo(type, extra, infoBody);
}
}
2. Play分析
從PlayerServer開(kāi)始跟蹤,調(diào)用到PlayerServer的OnPlay()方法。
int32_tPlayerServer::Play()
{
......
if (lastOpStatus_ == PLAYER_PREPARED || lastOpStatus_ == PLAYER_PLAYBACK_COMPLETE ||
lastOpStatus_ == PLAYER_PAUSED) {
return OnPlay();
} else {
return MSERR_INVALID_OPERATION;
}
}
在OnPlay中會(huì)啟動(dòng)一個(gè)任務(wù),在任務(wù)中獲取當(dāng)前的狀態(tài),然后調(diào)用當(dāng)前狀態(tài)的Play()方法。
int32_tPlayerServer::OnPlay()
{
......
auto playingTask = std::make_sharedvoid<>>([this]() {
auto currState = std::static_pointer_cast(GetCurrState());
(void)currState->Play();
});
int ret = taskMgr_.LaunchTask(playingTask, PlayerServerTaskType::STATE_CHANGE);
lastOpStatus_ = PLAYER_STARTED;
return MSERR_OK;
}
前面調(diào)用了PrepareAsync,所以當(dāng)前的狀態(tài)是Prepared,調(diào)用到了PreparedState的Play()方法,這個(gè)方法還是按照之前Prepare的方式,調(diào)回到PlayerServer的HandlePlay()。
int32_tPlayerServer::Play()
{
return server_.HandlePlay();
}
在PlayServer中通過(guò)播放引擎繼續(xù)向下調(diào)用。
int32_tPlayerServer::HandlePlay()
{
int32_t ret = playerEngine_->Play();
CHECK_AND_RETURN_RET_LOG(ret == MSERR_OK, MSERR_INVALID_OPERATION, "Engine Play Failed!");
return MSERR_OK;
}
在PlayerEngineGstImpl的Play()方法會(huì)繼續(xù)調(diào)用playBinCtrler_的Play()方法。
int32_tPlayerEngineGstImpl::Play()
{
......
playBinCtrler_->Play();
return MSERR_OK;
}
PlayBinCtrlerBase的Play()方法根據(jù)當(dāng)前的State,調(diào)用currSate->Play()。
int32_tPlayBinCtrlerBase::Play()
{
......
auto currState = std::static_pointer_cast(GetCurrState());
int32_t ret = currState->Play();
return MSERR_OK;
}
在PreparedState的Play()方法中改變了PlayBin的狀態(tài)為playing。
int32_tPlayBinCtrlerBase::Play()
{
GstStateChangeReturn ret;
return ChangePlayBinState(GST_STATE_PLAYING, ret);
}
ChangePlayBinState主要是調(diào)用了gst_element_set_state(GST_ELEMENT_CAST(ctrler_.playbin_),GST_STATE_PLAYING),這個(gè)直接調(diào)用了gstreamer三方庫(kù)的實(shí)現(xiàn),調(diào)用完這個(gè)方法以后,gstreamer就開(kāi)始進(jìn)行播放了。
int32_tPlayBinCtrlerBase::ChangePlayBinState(GstStatetargetState,GstStateChangeReturn&ret)
{
......
ret = gst_element_set_state(GST_ELEMENT_CAST(ctrler_.playbin_), targetState);
if (ret == GST_STATE_CHANGE_FAILURE) {
MEDIA_LOGE("Failed to change playbin's state to %{public}s", gst_element_state_get_name(targetState));
return MSERR_INVALID_OPERATION;
}
return MSERR_OK;
}
六、總結(jié)
本篇文章主要從PlayerServer播放服務(wù)開(kāi)始分析音視頻播放的流程,涉及到gstreamer引擎的調(diào)用,相對(duì)于多媒體播放框架來(lái)說(shuō),更加底層,便于熟悉從框架到gstreamer的整體流程。
更多熱點(diǎn)文章閱讀
- 玩嗨OpenHarmony:基于OpenHarmony的智能助老服務(wù)機(jī)器人
- 玩嗨OpenHarmony:基于OpenHarmony的智慧農(nóng)業(yè)環(huán)境監(jiān)控系統(tǒng)
- 首個(gè)通過(guò)OpenHarmony兼容性測(cè)評(píng)的全場(chǎng)景實(shí)驗(yàn)箱
- 基于OpenHarmony的智能門(mén)禁系統(tǒng),讓出行更便捷
-
OpenHarmony 3.2 Beta多媒體系列:音視頻播放框架
提示:本文由電子發(fā)燒友社區(qū)發(fā)布,轉(zhuǎn)載請(qǐng)注明以上來(lái)源。如需社區(qū)合作及入群交流,請(qǐng)?zhí)砑游⑿臙EFans0806,或者發(fā)郵箱liuyong@huaqiu.com。
原文標(biāo)題:OpenHarmony 3.2 Beta多媒體系列:音視頻播放gstreamer
文章出處:【微信公眾號(hào):電子發(fā)燒友開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
電子發(fā)燒友
+關(guān)注
關(guān)注
33文章
549瀏覽量
32886 -
開(kāi)源社區(qū)
+關(guān)注
關(guān)注
0文章
93瀏覽量
397
原文標(biāo)題:OpenHarmony 3.2 Beta多媒體系列:音視頻播放gstreamer
文章出處:【微信號(hào):HarmonyOS_Community,微信公眾號(hào):電子發(fā)燒友開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論