* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "interstitial_controller.h"
#include <algorithm>
#include <cmath>
#include "common/media_source.h"
#include "common/log.h"
#include "interstitial_scheduler.h"
#include "interstitial_vod_strategies.h"
#include "interstitial_live_strategies.h"
#include "media_demuxer.h"
namespace OHOS {
namespace Media {
namespace {
constexpr OHOS::HiviewDFX::HiLogLabel LABEL = {LOG_CORE, LOG_DOMAIN_DEMUXER, "InterstitialController"};
constexpr int64_t US_PER_MS = 1000;
constexpr int64_t AD_EXPIRE_WINDOW_MS = 200;
constexpr int64_t ADS_SEEK_POS = 0;
constexpr float ADS_PLAYBACK_SPEED = 1.0f;
}
InterstitialController::InterstitialController() = default;
InterstitialController::~InterstitialController() = default;
Status InterstitialController::Init(const std::shared_ptr<MediaDemuxer>& mainDemuxer,
const std::shared_ptr<MediaSyncManager>& syncMgr)
{
FALSE_RETURN_V_MSG_E(mainDemuxer, Status::ERROR_INVALID_PARAMETER, "Init failed: mainDemuxer is null");
mainDemuxer_ = mainDemuxer;
syncMgr_ = syncMgr;
MEDIA_LOG_I("Init success, syncMgr is null=" PUBLIC_LOG_D32, syncMgr == nullptr);
return Status::OK;
}
void InterstitialController::SetSyncCenter(std::shared_ptr<MediaSyncManager> syncMgr)
{
syncMgr_ = std::move(syncMgr);
MEDIA_LOG_I("SetSyncCenter OK");
}
void InterstitialController::SetScheduler(std::shared_ptr<InterstitialScheduler> scheduler)
{
scheduler_ = scheduler;
MEDIA_LOG_I("SetScheduler success");
}
void InterstitialController::SetMainContentUri(const std::string& uri)
{
mainContentUri_ = uri;
MEDIA_LOG_I("SetMainContentUri: " PUBLIC_LOG_S, uri.c_str());
}
void InterstitialController::SetLiveSource(bool isLive)
{
isLive_.store(isLive);
if (isLive) {
scheduleStrategy_ = std::make_shared<LiveScheduleStrategy>();
} else {
scheduleStrategy_ = std::make_shared<VodScheduleStrategy>();
}
if (scheduler_) {
scheduler_->SetScheduleStrategy(scheduleStrategy_);
}
MEDIA_LOG_I("SetLiveSource: isLive=" PUBLIC_LOG_D32, isLive);
}
void InterstitialController::SetAdsEventCallback(AdsEventCallback cb)
{
adsEventCallback_ = cb;
MEDIA_LOG_I("SetAdsEventCallback success");
}
void InterstitialController::SetSpeedChangeCallback(SpeedChangeCallback cb)
{
speedChangeCallback_ = cb;
MEDIA_LOG_I("SetSpeedChangeCallback success");
}
Status InterstitialController::AddAdsMediaSource(const std::shared_ptr<MediaSource>& source,
int64_t startMs, std::string& outId)
{
FALSE_RETURN_V_MSG_E(source, Status::ERROR_INVALID_PARAMETER, "AddAdsMediaSource failed: source is null");
std::string id;
{
std::lock_guard<std::mutex> lock(adMutex_);
std::string uri = source->GetSourceUri();
for (const auto& kv : entries_) {
if (kv.second.resourceUri == uri && kv.second.startMs == startMs) {
outId = kv.first;
MEDIA_LOG_I("AddAdsMediaSource: duplicate uri=" PUBLIC_LOG_S ", startMs=" PUBLIC_LOG_D64
", existing id=" PUBLIC_LOG_S, uri.c_str(), startMs, kv.first.c_str());
return Status::OK;
}
}
id = CreateAndAddEntry(uri, startMs, source);
outId = id;
MEDIA_LOG_I("AddAdsMediaSource: id=" PUBLIC_LOG_S ", uri=" PUBLIC_LOG_S ", startMs=" PUBLIC_LOG_D64,
id.c_str(), uri.c_str(), startMs);
}
if (startMs == 0 && playState_ == InterstitialPlayState::IDLE && preRollAdId_.empty()) {
MEDIA_LOG_I("AddAdsMediaSource: pre-roll ad detected, will trigger on DoStart");
preRollAdId_ = id;
}
return Status::OK;
}
Status InterstitialController::RemoveAdsMediaSource(const std::string& id)
{
std::lock_guard<std::mutex> lock(adMutex_);
auto it = entries_.find(id);
if (it == entries_.end()) {
MEDIA_LOG_W("RemoveAdsMediaSource: ad id=" PUBLIC_LOG_S " not found", id.c_str());
return Status::ERROR_INVALID_PARAMETER;
}
if (playState_ == InterstitialPlayState::PLAYING && currentAdId_ == id) {
MEDIA_LOG_W("RemoveAdsMediaSource: cannot remove currently playing ad id=" PUBLIC_LOG_S, id.c_str());
return Status::OK;
}
entries_.erase(it);
MEDIA_LOG_I("RemoveAdsMediaSource: id=" PUBLIC_LOG_S, id.c_str());
return Status::OK;
}
Status InterstitialController::DisableAllAdsMediaSource()
{
isDisableAds_.store(true);
std::lock_guard<std::mutex> lock(adMutex_);
if (playState_ != InterstitialPlayState::PLAYING) {
entries_.clear();
return Status::OK;
}
MEDIA_LOG_W("DisableAllAdsMediaSource: currently playing ad id=" PUBLIC_LOG_S, currentAdId_.c_str());
auto it = entries_.find(currentAdId_);
if (it != entries_.end()) {
InterstitialEntry currentEntry = it->second;
entries_.clear();
entries_[currentAdId_] = currentEntry;
} else {
entries_.clear();
}
MEDIA_LOG_I("DisableAllAdsMediaSource success, remaining=" PUBLIC_LOG_ZU, entries_.size());
return Status::OK;
}
Status InterstitialController::SkipCurrentAdsMediaSource()
{
{
std::lock_guard<std::mutex> lock(adMutex_);
if (playState_ != InterstitialPlayState::PLAYING) {
MEDIA_LOG_W("SkipCurrentAdsMediaSource: not playing ad, state=" PUBLIC_LOG_U32, playState_.load());
return Status::OK;
}
MEDIA_LOG_I("SkipCurrentAdsMediaSource: skipping current ad id=" PUBLIC_LOG_S, currentAdId_.c_str());
}
FinishCurrentAdAndContinue(AdsEndReason::SKIPPED);
return Status::OK;
}
void InterstitialController::OnPreloadTick()
{
MEDIA_LOG_I("OnPreloadTick");
FALSE_RETURN_MSG_W(!isDisableAds_.load(), "InterstitialController::OnPreloadTick failed: ads are disabled");
int64_t targetStartMs = -1;
{
std::lock_guard<std::mutex> lock(adMutex_);
if (playState_ != InterstitialPlayState::IDLE) {
MEDIA_LOG_W("is playing ad");
return;
}
FALSE_RETURN_MSG_W(syncMgr_, "InterstitialController::OnPreloadTick failed: syncMgr is null");
int64_t currentPosMs = syncMgr_->GetMediaTimeNow() / US_PER_MS;
std::string nextId = FindNextAdId(currentPosMs);
if (nextId.empty()) {
return;
}
targetStartMs = entries_[nextId].startMs;
}
if (!TrySwitchAdCandidates(targetStartMs)) {
DoResume();
}
}
void InterstitialController::OnAdEos()
{
{
std::lock_guard<std::mutex> lock(adMutex_);
if (playState_ != InterstitialPlayState::PLAYING) {
MEDIA_LOG_W("OnAdEos: not playing ad, state=" PUBLIC_LOG_U32, playState_.load());
return;
}
MEDIA_LOG_I("OnAdEos: ad EOS detected, currentAdId=" PUBLIC_LOG_S, currentAdId_.c_str());
}
FinishCurrentAdAndContinue(AdsEndReason::COMPLETED);
}
void InterstitialController::TryPreRollAd()
{
if (preRollAdId_.empty()) {
return;
}
if (isDisableAds_.load() || playState_ != InterstitialPlayState::IDLE) {
MEDIA_LOG_I("TryPreRollAd: skipped, disabled=" PUBLIC_LOG_D32 ", state=" PUBLIC_LOG_U32,
isDisableAds_.load(), playState_.load());
preRollAdId_.clear();
return;
}
{
std::lock_guard<std::mutex> lock(adMutex_);
auto it = entries_.find(preRollAdId_);
if (it == entries_.end() || it->second.played) {
preRollAdId_.clear();
return;
}
}
MEDIA_LOG_I("TryPreRollAd: triggering pre-roll ad " PUBLIC_LOG_S, preRollAdId_.c_str());
if (!TrySwitchAdCandidates(0)) {
DoResume();
}
}
void InterstitialController::OnStop()
{
std::lock_guard<std::mutex> lock(adMutex_);
if (playState_ == InterstitialPlayState::PLAYING) {
MEDIA_LOG_W("OnStop: currently playing ad, will stop");
SetPlayState(InterstitialPlayState::IDLE);
}
entries_.clear();
currentAdId_.clear();
preRollAdId_.clear();
resumePointMs_ = 0;
MEDIA_LOG_I("OnStop success");
}
void InterstitialController::OnSeek(int64_t seekTargetMs)
{
MEDIA_LOG_I("OnSeek: seekTargetMs=" PUBLIC_LOG_D64, seekTargetMs);
if (scheduleStrategy_ && !scheduleStrategy_->ShouldHandleSeek()) {
MEDIA_LOG_I("OnSeek: strategy says no-op");
return;
}
std::string preRollToTrigger;
{
std::lock_guard<std::mutex> lock(adMutex_);
for (auto& pair : entries_) {
if (pair.second.startMs < seekTargetMs) {
pair.second.played = true;
} else {
pair.second.played = false;
}
}
if (seekTargetMs == 0 && !preRollAdId_.empty()) {
auto it = entries_.find(preRollAdId_);
if (it != entries_.end() && !it->second.played) {
preRollToTrigger = preRollAdId_;
}
}
}
FALSE_RETURN_MSG(!preRollToTrigger.empty(), "OnSeek: no pre-roll ad to trigger");
if (!TrySwitchAdCandidates(0)) {
DoResume();
}
}
bool InterstitialController::IsPlayingInterstitial() const
{
return playState_ == InterstitialPlayState::PLAYING;
}
bool InterstitialController::IsAdsDisabled() const
{
return isDisableAds_.load();
}
InterstitialPlayState InterstitialController::GetPlayState() const
{
return playState_.load();
}
bool InterstitialController::HasPendingEvents() const
{
std::lock_guard<std::mutex> lock(adMutex_);
for (const auto& pair : entries_) {
if (!pair.second.played) {
return true;
}
}
return false;
}
bool InterstitialController::IsLiveSource() const
{
return isLive_.load();
}
std::shared_ptr<IScheduleStrategy> InterstitialController::GetScheduleStrategy() const
{
return scheduleStrategy_;
}
bool InterstitialController::DoSwitch(const std::string& id)
{
MEDIA_LOG_I("DoSwitch: id=" PUBLIC_LOG_S, id.c_str());
FALSE_RETURN_V_MSG_E(syncMgr_, false, "DoSwitch: syncMgr_ is null, cannot switch");
if (scheduleStrategy_ && scheduleStrategy_->ShouldSaveResumePoint() && resumePointMs_ == 0) {
resumePointMs_ = syncMgr_->GetMediaTimeNow() / US_PER_MS;
MEDIA_LOG_I("DoSwitch: recorded resumePointMs=" PUBLIC_LOG_D64, resumePointMs_.load());
}
auto it = entries_.find(id);
if (it == entries_.end()) {
MEDIA_LOG_E("DoSwitch: entry not found for id=" PUBLIC_LOG_S, id.c_str());
return false;
}
originalSpeed_ = syncMgr_->GetPlaybackRate();
MEDIA_LOG_I("DoSwitch: saved originalSpeed=" PUBLIC_LOG_F, originalSpeed_.load());
currentAdId_ = id;
auto preStartAction = [this]() {
auto it = entries_.find(currentAdId_);
if (it != entries_.end()) {
int64_t durationUs = 0;
if (mainDemuxer_->GetDuration(durationUs) && durationUs > 0) {
it->second.durationMs = durationUs / US_PER_MS;
MEDIA_LOG_I("DoSwitch: updated durationMs=" PUBLIC_LOG_D64 " for id=" PUBLIC_LOG_S,
durationUs, currentAdId_.c_str());
}
EmitStartEvent(it->second);
}
};
SetPlayState(InterstitialPlayState::PLAYING);
auto ret = DoSourceSwitch(it->second.resourceUri, ADS_SEEK_POS, ADS_PLAYBACK_SPEED, preStartAction);
if (ret != Status::OK) {
MEDIA_LOG_E("DoSwitch: DoSourceSwitch failed for id=" PUBLIC_LOG_S, id.c_str());
SetPlayState(InterstitialPlayState::IDLE);
currentAdId_.clear();
return false;
}
MEDIA_LOG_I("DoSwitch success: change to ads");
return true;
}
bool InterstitialController::TrySwitchAdCandidates(int64_t startMs)
{
MEDIA_LOG_I("TrySwitchAdCandidates: startMs=" PUBLIC_LOG_D64, startMs);
std::vector<std::string> candidates;
{
std::lock_guard<std::mutex> lock(adMutex_);
candidates = CollectSameStartMsAds(startMs);
}
for (const auto& id : candidates) {
if (DoSwitch(id)) {
return true;
}
std::lock_guard<std::mutex> lock(adMutex_);
entries_[id].played = true;
}
return false;
}
void InterstitialController::DoResume()
{
int64_t seekMs = resumePointMs_;
if (scheduleStrategy_) {
seekMs = scheduleStrategy_->GetResumeSeekMs(resumePointMs_, mainDemuxer_);
}
MEDIA_LOG_I("DoResume: mainContentUri=" PUBLIC_LOG_S ", seekMs=" PUBLIC_LOG_D64,
mainContentUri_.c_str(), seekMs);
if (mainContentUri_.empty() || !syncMgr_) {
MEDIA_LOG_E("DoResume: cannot resume, mainContentUri is empty=%{public}d, syncMgr is null=%{public}d",
mainContentUri_.empty(), !syncMgr_);
SetPlayState(InterstitialPlayState::IDLE);
currentAdId_.clear();
resumePointMs_ = 0;
return;
}
float speed = originalSpeed_.load();
DoSourceSwitch(mainContentUri_, seekMs, speed);
SetPlayState(InterstitialPlayState::IDLE);
currentAdId_.clear();
resumePointMs_ = 0;
MEDIA_LOG_I("DoResume success: change to main, speed=" PUBLIC_LOG_F, speed);
}
Status InterstitialController::DoSourceSwitch(const std::string& uri, int64_t seekMs, float speed,
std::function<void()> preStartAction)
{
MEDIA_LOG_I("DoSourceSwitch: uri=" PUBLIC_LOG_S ", seekMs=" PUBLIC_LOG_D64 ", speed=" PUBLIC_LOG_F,
uri.c_str(), seekMs, speed);
FALSE_RETURN_V_MSG_E(syncMgr_, Status::ERROR_INVALID_OPERATION, "DoSourceSwitch: syncMgr_ is null, cannot switch");
uint32_t capturedBitRate = mainDemuxer_->GetCurrentBitRate();
MEDIA_LOG_I("DoSourceSwitch: captured bitrate=" PUBLIC_LOG_U32, capturedBitRate);
if (speedChangeCallback_) {
speedChangeCallback_(speed);
}
mainDemuxer_->HandleForSourceSwitch();
auto source = std::make_shared<MediaSource>(uri);
auto status = mainDemuxer_->SetDataSource(source);
FALSE_RETURN_V_MSG_E(status == Status::OK, status, "DoSourceSwitch: SetDataSource failed");
status = mainDemuxer_->ReselectTracks();
FALSE_RETURN_V_MSG_E(status == Status::OK, status, "DoSourceSwitch: ReselectTracks failed");
seekMs = ClampSeekMs(seekMs);
int64_t realSeekTime = 0;
status = mainDemuxer_->SeekTo(seekMs, Plugins::SeekMode::SEEK_CLOSEST_INNER, realSeekTime);
FALSE_RETURN_V_MSG_E(status == Status::OK, status, "DoSourceSwitch: SeekTo failed");
MEDIA_LOG_I("DoSourceSwitch: SeekTo success, realSeekTime=" PUBLIC_LOG_D64, realSeekTime);
syncMgr_->Seek(seekMs * US_PER_MS, true);
MEDIA_LOG_I("DoSourceSwitch: syncMgr_->Seek done");
if (preStartAction) {
preStartAction();
}
status = mainDemuxer_->Start();
FALSE_RETURN_V_MSG_E(status == Status::OK, status, "DoSourceSwitch: Start failed");
if (capturedBitRate > 0) {
mainDemuxer_->SelectBitRate(capturedBitRate, false, true);
}
MEDIA_LOG_I("DoSourceSwitch success");
return Status::OK;
}
void InterstitialController::FinishCurrentAdAndContinue(AdsEndReason reason)
{
InterstitialEntry currentEntry;
int64_t startMs = -1;
{
std::lock_guard<std::mutex> lock(adMutex_);
auto it = entries_.find(currentAdId_);
if (it != entries_.end()) {
currentEntry = it->second;
startMs = it->second.startMs;
it->second.played = true;
}
}
EmitEndEvent(currentEntry, reason);
if (startMs >= 0 && TrySwitchAdCandidates(startMs)) {
return;
}
{
std::lock_guard<std::mutex> lock(adMutex_);
SetPlayState(InterstitialPlayState::IDLE);
currentAdId_.clear();
}
DoResume();
}
int64_t InterstitialController::ClampSeekMs(int64_t seekMs)
{
int64_t durationUs = 0;
if (mainDemuxer_->GetDuration(durationUs) && durationUs > 0) {
int64_t durationMs = durationUs / US_PER_MS;
if (seekMs > durationMs) {
MEDIA_LOG_W("ClampSeekMs: seekMs " PUBLIC_LOG_D64 " exceeds duration " PUBLIC_LOG_D64 ", clamping",
seekMs, durationMs);
return durationMs;
}
}
return seekMs;
}
std::string InterstitialController::FindNextAdId(int64_t currentPosMs)
{
MEDIA_LOG_I("FindNextAdId: currentPosMs=" PUBLIC_LOG_D64, currentPosMs);
FALSE_RETURN_V_MSG_E(syncMgr_, "", "FindNextAdId: syncMgr is null");
std::string bestId;
int64_t bestStartMs = INT64_MAX;
float speed = syncMgr_->GetPlaybackRate();
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
if (it->second.played) {
continue;
}
int64_t startMs = it->second.startMs;
int64_t endMs = startMs + static_cast<int64_t>(AD_EXPIRE_WINDOW_MS * std::max(ADS_PLAYBACK_SPEED, speed));
if (currentPosMs > endMs) {
it->second.played = true;
continue;
}
if (startMs <= currentPosMs && startMs < bestStartMs) {
bestStartMs = startMs;
bestId = it->first;
}
}
return bestId;
}
std::vector<std::string> InterstitialController::CollectSameStartMsAds(int64_t startMs)
{
std::vector<std::pair<int32_t, std::string>> ordered;
for (auto& pair : entries_) {
if (!pair.second.played && pair.second.startMs == startMs) {
ordered.push_back({pair.second.order, pair.first});
}
}
std::sort(ordered.begin(), ordered.end());
std::vector<std::string> result;
result.reserve(ordered.size());
for (auto& p : ordered) {
result.push_back(p.second);
}
return result;
}
std::string InterstitialController::GenerateAdId()
{
int32_t counter = adCounter_.fetch_add(1);
return "ad_" + std::to_string(counter);
}
std::string InterstitialController::CreateAndAddEntry(const std::string& uri, int64_t startMs,
const std::shared_ptr<MediaSource>& source)
{
int32_t seq = adCounter_.fetch_add(1);
std::string id = "ad_" + std::to_string(seq);
InterstitialEntry entry;
entry.eventId = id;
entry.resourceUri = uri;
entry.startMs = startMs;
entry.mediaSource = source;
entry.order = seq;
entries_[id] = std::move(entry);
return id;
}
void InterstitialController::EmitStartEvent(const InterstitialEntry& entry)
{
if (!adsEventCallback_) {
return;
}
AdsChangeEvent event;
event.type = AdsEventType::START;
event.eventId = entry.eventId;
event.startMs = entry.startMs;
event.durationMs = entry.durationMs;
MEDIA_LOG_I("EmitStartEvent: eventId=" PUBLIC_LOG_S ", startMs=" PUBLIC_LOG_D64 ", durationMs=" PUBLIC_LOG_D64,
entry.eventId.c_str(), entry.startMs, entry.durationMs);
adsEventCallback_(event);
}
void InterstitialController::EmitEndEvent(const InterstitialEntry& entry, AdsEndReason reason)
{
if (!adsEventCallback_ || entry.eventId.empty()) {
return;
}
AdsChangeEvent event;
event.type = AdsEventType::END;
event.eventId = entry.eventId;
event.startMs = entry.startMs;
event.durationMs = entry.durationMs;
event.reason = reason;
MEDIA_LOG_I("EmitEndEvent: eventId=" PUBLIC_LOG_S ", startMs=" PUBLIC_LOG_D64 ", durationMs=" PUBLIC_LOG_D64
", reason=" PUBLIC_LOG_D32, entry.eventId.c_str(), entry.startMs, entry.durationMs, reason);
adsEventCallback_(event);
}
void InterstitialController::SetPlayState(InterstitialPlayState state)
{
playState_.store(state);
MEDIA_LOG_I("SetPlayState: " PUBLIC_LOG_U32, state);
}
}
}