Commit 1cf24333 authored by Mohamed Chibani's avatar Mohamed Chibani

mute in conference: rework mute/un-mute of local host

When a call is added to a conference, the control of the mute/un-mute
state of the media of the call is taken over by the conference, and
the mute state of the participating calls will be controlled by the
state of the local host set in the conference, which basically consists
of attaching/detaching the source to/from the mixer. Currently the local
host mute state might not be correctly initialized, leading to
inconsistent mute states.
The proposed changes will correctly set the local host state according
to the initial mute states of each call joining the conference.

Gitlab: #576

Change-Id: I0a746aae82da57222cc7ff91c2e39a1a2bbaff8e
parent 95f99b2f
......@@ -380,8 +380,10 @@ Call::getDetails() const
{DRing::Call::Details::CONF_ID, confID_},
{DRing::Call::Details::TIMESTAMP_START, std::to_string(timestamp_start_)},
{DRing::Call::Details::ACCOUNTID, getAccountId()},
{DRing::Call::Details::AUDIO_MUTED, std::string(bool_to_str(isAudioMuted()))},
{DRing::Call::Details::VIDEO_MUTED, std::string(bool_to_str(isVideoMuted()))},
{DRing::Call::Details::AUDIO_MUTED,
std::string(bool_to_str(isCaptureDeviceMuted(MediaType::MEDIA_AUDIO)))},
{DRing::Call::Details::VIDEO_MUTED,
std::string(bool_to_str(isCaptureDeviceMuted(MediaType::MEDIA_VIDEO)))},
{DRing::Call::Details::AUDIO_ONLY, std::string(bool_to_str(not hasVideo()))},
};
}
......
......@@ -373,8 +373,7 @@ public: // media management
// Media status methods
virtual bool hasVideo() const = 0;
virtual bool isAudioMuted() const = 0;
virtual bool isVideoMuted() const = 0;
virtual bool isCaptureDeviceMuted(const MediaType& mediaType) const = 0;
/**
* A Call can be in a conference. If this is the case, the other side
......
......@@ -94,8 +94,10 @@ Conference::Conference()
subCalls.erase(it->second);
std::string_view peerID = string_remove_suffix(uri, '@');
auto isModerator = shared->isModerator(peerID);
if (uri.empty())
if (uri.empty()) {
peerID = "host"sv;
isLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO);
}
auto isModeratorMuted = shared->isMuted(peerID);
newInfo.emplace_back(ParticipantInfo {std::move(uri),
"",
......@@ -249,16 +251,129 @@ Conference::createConfAVStream(const StreamData& StreamData,
}
#endif // ENABLE_PLUGIN
void
Conference::setMediaSourceState(MediaType type, bool muted)
{
if (type == MediaType::MEDIA_AUDIO) {
audioSourceMuted_ = muted ? MediaSourceState::MUTED : MediaSourceState::UNMUTED;
} else if (type == MediaType::MEDIA_VIDEO) {
videoSourceMuted_ = muted ? MediaSourceState::MUTED : MediaSourceState::UNMUTED;
} else {
JAMI_ERR("Unsupported media type");
}
}
MediaSourceState
Conference::getMediaSourceState(MediaType type) const
{
if (type == MediaType::MEDIA_AUDIO) {
return audioSourceMuted_;
} else if (type == MediaType::MEDIA_VIDEO) {
return videoSourceMuted_;
} else {
JAMI_ERR("Unsupported media type");
return MediaSourceState::NONE;
}
}
bool
Conference::isMediaSourceMuted(MediaType type) const
{
if (type == MediaType::MEDIA_AUDIO) {
return audioSourceMuted_ == MediaSourceState::MUTED;
} else if (type == MediaType::MEDIA_VIDEO) {
return videoSourceMuted_ == MediaSourceState::MUTED;
} else {
JAMI_ERR("Unsupported media type");
return false;
}
}
void
Conference::takeOverMediaSourceControl(const std::string& callId)
{
auto call = getCall(callId);
if (not call) {
JAMI_ERR("No call matches participant %s", callId.c_str());
return;
}
auto mediaList = call->getMediaAttributeList();
std::vector<MediaType> mediaTypeList {MediaType::MEDIA_AUDIO, MediaType::MEDIA_VIDEO};
for (auto mediaType : mediaTypeList) {
// Try to find a media with a valid source type
auto check = [mediaType](auto const& mediaAttr) {
return (mediaAttr.type_ == mediaType and mediaAttr.sourceType_ != MediaSourceType::NONE);
};
auto iter = std::find_if(mediaList.begin(), mediaList.end(), check);
if (iter == mediaList.end()) {
// Nothing to do if the call does not have a media that uses with
// a valid source type.
JAMI_DBG("[Call: %s] Does not have an active [%s] media source",
callId.c_str(),
MediaAttribute::mediaTypeToString(mediaType));
return;
}
if (getMediaSourceState(iter->type_) == MediaSourceState::NONE) {
// If the source state for the specified media type is not set
// yet, the state will initialized using the state of the first
// participant with a valid media source.
setMediaSourceState(iter->type_, iter->muted_);
} else {
// To mute the local source, all the sources of the participating
// calls must be muted.
setMediaSourceState(iter->type_, iter->muted_ && isMediaSourceMuted(iter->type_));
}
// Un-mute media in the call. The mute/un-mute state will be handled
// by the conference/mixer from now on.
iter->muted_ = false;
}
// Update the media states in the newly added call.
call->requestMediaChange(MediaAttribute::mediaAttributesToMediaMaps(mediaList));
// Notify the client
for (auto mediaType : mediaTypeList) {
if (mediaType == MediaType::MEDIA_AUDIO) {
bool muted = isMediaSourceMuted(MediaType::MEDIA_AUDIO);
JAMI_WARN("Take over [AUDIO] control from call %s - current local source state [%s]",
callId.c_str(),
muted ? "muted" : "un-muted");
emitSignal<DRing::CallSignal::AudioMuted>(id_, muted);
} else {
bool muted = isMediaSourceMuted(MediaType::MEDIA_VIDEO);
JAMI_WARN("Take over [VIDEO] control from call %s - current local source state [%s]",
callId.c_str(),
muted ? "muted" : "un-muted");
emitSignal<DRing::CallSignal::VideoMuted>(id_, muted);
}
}
}
void
Conference::add(const std::string& participant_id)
{
JAMI_DBG("Adding call %s to conference %s", participant_id.c_str(), id_.c_str());
if (participants_.insert(participant_id).second) {
// Check if participant was muted before conference
if (auto call = getCall(participant_id)) {
if (call->isPeerMuted()) {
participantsMuted_.emplace(string_remove_suffix(call->getPeerNumber(), '@'));
}
// When a call joins a conference, the control if the media
// source sates (mainly mute/un-mute states) will be handled
// by the conference.
takeOverMediaSourceControl(participant_id);
}
if (auto call = getCall(participant_id)) {
auto w = call->getAccount();
auto account = w.lock();
......@@ -519,7 +634,7 @@ Conference::bindParticipant(const std::string& participant_id)
// Bind local participant to other participants only if the
// local is attached to the conference.
if (getState() == State::ACTIVE_ATTACHED) {
if (isMuted("host"sv))
if (isMediaSourceMuted(MediaType::MEDIA_AUDIO))
rbPool.bindHalfDuplexOut(RingBufferPool::DEFAULT_ID, participant_id);
else
rbPool.bindCallID(participant_id, RingBufferPool::DEFAULT_ID);
......@@ -802,13 +917,13 @@ Conference::muteParticipant(const std::string& participant_id, const bool& state
auto isHostMuted = isMuted("host"sv);
if (state and not isHostMuted) {
participantsMuted_.emplace("host"sv);
if (not audioMuted_) {
if (not isMediaSourceMuted(MediaType::MEDIA_AUDIO)) {
JAMI_DBG("Mute host");
unbindHost();
}
} else if (not state and isHostMuted) {
participantsMuted_.erase("host");
if (not audioMuted_) {
if (not isMediaSourceMuted(MediaType::MEDIA_AUDIO)) {
JAMI_DBG("Unmute host");
bindHost();
}
......@@ -844,7 +959,7 @@ Conference::updateMuted()
if (peerID.empty()) {
peerID = "host"sv;
info.audioModeratorMuted = isMuted(peerID);
info.audioLocalMuted = audioMuted_;
info.audioLocalMuted = isMediaSourceMuted(MediaType::MEDIA_AUDIO);
} else {
info.audioModeratorMuted = isMuted(peerID);
if (auto call = getCallFromPeerID(peerID))
......@@ -949,30 +1064,37 @@ void
Conference::muteLocalHost(bool is_muted, const std::string& mediaType)
{
if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_AUDIO) == 0) {
if (is_muted == audioMuted_)
if (is_muted == isMediaSourceMuted(MediaType::MEDIA_AUDIO)) {
JAMI_DBG("Local audio source already in [%s] state", is_muted ? "muted" : "un-muted");
return;
}
auto isHostMuted = isMuted("host"sv);
if (is_muted and not audioMuted_ and not isHostMuted) {
JAMI_DBG("Local audio mute host");
if (is_muted and not isMediaSourceMuted(MediaType::MEDIA_AUDIO) and not isHostMuted) {
JAMI_DBG("Muting local audio source");
unbindHost();
} else if (not is_muted and audioMuted_ and not isHostMuted) {
JAMI_DBG("Local audio unmute host");
} else if (not is_muted and isMediaSourceMuted(MediaType::MEDIA_AUDIO) and not isHostMuted) {
JAMI_DBG("Un-muting local audio source");
bindHost();
}
audioMuted_ = is_muted;
setMediaSourceState(MediaType::MEDIA_AUDIO, is_muted);
updateMuted();
emitSignal<DRing::CallSignal::AudioMuted>(id_, is_muted);
return;
} else if (mediaType.compare(DRing::Media::Details::MEDIA_TYPE_VIDEO) == 0) {
#ifdef ENABLE_VIDEO
if (is_muted == videoMuted_)
if (is_muted == (isMediaSourceMuted(MediaType::MEDIA_VIDEO))) {
JAMI_DBG("Local video source already in [%s] state", is_muted ? "muted" : "un-muted");
return;
}
if (is_muted) {
if (auto mixer = getVideoMixer()) {
JAMI_DBG("Muting local video source");
mixer->stopInput();
}
} else {
if (auto mixer = getVideoMixer()) {
JAMI_DBG("Un-muting local video source");
mixer->switchInput(mediaInput_);
#ifdef ENABLE_PLUGIN
// Preview
......@@ -987,7 +1109,7 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType)
#endif
}
}
videoMuted_ = is_muted;
setMediaSourceState(MediaType::MEDIA_VIDEO, is_muted);
emitSignal<DRing::CallSignal::VideoMuted>(id_, is_muted);
return;
#endif
......
......@@ -158,6 +158,12 @@ struct ConfInfo : public std::vector<ParticipantInfo>
std::string toString() const;
};
enum class MediaSourceState : unsigned {
NONE = 0, // Not set yet
MUTED,
UNMUTED
};
using ParticipantSet = std::set<std::string>;
class Conference : public Recordable, public std::enable_shared_from_this<Conference>
......@@ -209,6 +215,25 @@ public:
const char* getStateStr() const { return getStateStr(confState_); }
/**
* Set the mute state of the local host
*/
void setMediaSourceState(MediaType type, bool muted);
/**
* Get the mute state of the local host
*/
MediaSourceState getMediaSourceState(MediaType type) const;
bool isMediaSourceMuted(MediaType type) const;
/**
* Take over media control from the call.
* When a call joins a conference, the media control (mainly mute/un-mute
* state of the local media source) will be handled by the conference and
* the mixer.
*/
void takeOverMediaSourceControl(const std::string& callId);
/**
* Add a new participant to the conference
*/
......@@ -336,8 +361,21 @@ private:
ConfInfo getConfInfoHostUri(std::string_view localHostURI, std::string_view destURI);
bool isHost(std::string_view uri) const;
bool audioMuted_ {false};
bool videoMuted_ {false};
/**
* If the local host is participating in the conference (attached
* mode ), these two variables will hold the media source states
* of the local host.
*
* NOTE:
* Currently, the conference and the client support only one stream
* per media type, even if the call supports an arbitrary number of
* streams per media type. Thus, these two variables will hold the
* media source states regardless of the media type (capture device,
* display, ...)
*/
MediaSourceState audioSourceMuted_ {MediaSourceState::NONE};
MediaSourceState videoSourceMuted_ {MediaSourceState::NONE};
bool localModAdded_ {false};
......
......@@ -50,17 +50,22 @@ constexpr static char HEIGHT[] = "HEIGHT";
} // namespace Details
namespace MediaAttributeKey {
constexpr static char MEDIA_TYPE[] = "MEDIA_TYPE"; // string
constexpr static char ENABLED[] = "ENABLED"; // bool
constexpr static char MUTED[] = "MUTED"; // bool
constexpr static char SOURCE[] = "SOURCE"; // string
constexpr static char LABEL[] = "LABEL"; // string
constexpr static char ON_HOLD[] = "ON_HOLD"; // bool
constexpr static char MEDIA_TYPE[] = "MEDIA_TYPE"; // string
constexpr static char ENABLED[] = "ENABLED"; // bool
constexpr static char MUTED[] = "MUTED"; // bool
constexpr static char SOURCE[] = "SOURCE"; // string
constexpr static char SOURCE_TYPE[] = "SOURCE_TYPE"; // string
constexpr static char LABEL[] = "LABEL"; // string
constexpr static char ON_HOLD[] = "ON_HOLD"; // bool
} // namespace MediaAttributeKey
namespace MediaAttributeValue {
constexpr static auto AUDIO = "MEDIA_TYPE_AUDIO";
constexpr static auto VIDEO = "MEDIA_TYPE_VIDEO";
constexpr static auto SRC_TYPE_NONE = "NONE";
constexpr static auto SRC_TYPE_CAPTURE_DEVICE = "CAPTURE_DEVICE";
constexpr static auto SRC_TYPE_DISPLAY = "DISPLAY";
constexpr static auto SRC_TYPE_FILE = "FILE";
} // namespace MediaAttributeValue
namespace MediaNegotiationStatusEvents {
......
......@@ -44,6 +44,10 @@ MediaAttribute::MediaAttribute(const DRing::MediaMap& mediaMap, bool secure)
if (pairBool.first)
sourceUri_ = pairString.second;
std::pair<bool, MediaSourceType> pairSrcType = getMediaSourceType(mediaMap);
if (pairSrcType.first)
sourceType_ = pairSrcType.second;
pairString = getStringValue(mediaMap, DRing::Media::MediaAttributeKey::LABEL);
if (pairBool.first)
label_ = pairString.second;
......@@ -96,6 +100,30 @@ MediaAttribute::getMediaType(const DRing::MediaMap& map)
return {true, type};
}
MediaSourceType
MediaAttribute::stringToMediaSourceType(const std::string& srcType)
{
if (srcType.compare(DRing::Media::MediaAttributeValue::SRC_TYPE_CAPTURE_DEVICE) == 0)
return MediaSourceType::CAPTURE_DEVICE;
if (srcType.compare(DRing::Media::MediaAttributeValue::SRC_TYPE_DISPLAY) == 0)
return MediaSourceType::DISPLAY;
if (srcType.compare(DRing::Media::MediaAttributeValue::SRC_TYPE_FILE) == 0)
return MediaSourceType::FILE;
return MediaSourceType::NONE;
}
std::pair<bool, MediaSourceType>
MediaAttribute::getMediaSourceType(const DRing::MediaMap& map)
{
const auto& iter = map.find(DRing::Media::MediaAttributeKey::SOURCE_TYPE);
if (iter == map.end()) {
JAMI_WARN("[MEDIA_TYPE] key not found in media map");
return {false, MediaSourceType::NONE};
}
return {true, stringToMediaSourceType(iter->second)};
}
std::pair<bool, bool>
MediaAttribute::getBoolValue(const DRing::MediaMap& map, const std::string& key)
{
......@@ -143,6 +171,20 @@ MediaAttribute::mediaTypeToString(MediaType type)
return nullptr;
}
char const*
MediaAttribute::mediaSourceTypeToString(MediaSourceType type)
{
if (type == MediaSourceType::NONE)
return DRing::Media::MediaAttributeValue::SRC_TYPE_NONE;
if (type == MediaSourceType::CAPTURE_DEVICE)
return DRing::Media::MediaAttributeValue::SRC_TYPE_CAPTURE_DEVICE;
if (type == MediaSourceType::DISPLAY)
return DRing::Media::MediaAttributeValue::SRC_TYPE_DISPLAY;
if (type == MediaSourceType::FILE)
return DRing::Media::MediaAttributeValue::SRC_TYPE_FILE;
return nullptr;
}
bool
MediaAttribute::hasMediaType(const std::vector<MediaAttribute>& mediaList, MediaType type)
{
......@@ -163,6 +205,8 @@ MediaAttribute::toMediaMap(const MediaAttribute& mediaAttr)
mediaMap.emplace(DRing::Media::MediaAttributeKey::ENABLED, boolToString(mediaAttr.enabled_));
mediaMap.emplace(DRing::Media::MediaAttributeKey::MUTED, boolToString(mediaAttr.muted_));
mediaMap.emplace(DRing::Media::MediaAttributeKey::SOURCE, mediaAttr.sourceUri_);
mediaMap.emplace(DRing::Media::MediaAttributeKey::SOURCE_TYPE,
mediaSourceTypeToString(mediaAttr.sourceType_));
mediaMap.emplace(DRing::Media::MediaAttributeKey::ON_HOLD, boolToString(mediaAttr.onHold_));
return mediaMap;
......@@ -197,6 +241,8 @@ MediaAttribute::toString(bool full) const
descr << " ";
descr << "source [" << sourceUri_ << "]";
descr << " ";
descr << "src type [" << mediaSourceTypeToString(sourceType_) << "]";
descr << " ";
descr << "secure " << (secure_ ? "[YES]" : "[NO]");
}
......
......@@ -56,6 +56,10 @@ public:
static std::pair<bool, MediaType> getMediaType(const DRing::MediaMap& map);
static MediaSourceType stringToMediaSourceType(const std::string& mediaSourceType);
static std::pair<bool, MediaSourceType> getMediaSourceType(const DRing::MediaMap& map);
static std::pair<bool, bool> getBoolValue(const DRing::MediaMap& mediaMap,
const std::string& key);
......@@ -71,6 +75,9 @@ public:
// Return a string of the media type
static char const* mediaTypeToString(MediaType type);
// Return a string of the media source type
static char const* mediaSourceTypeToString(MediaSourceType type);
// Convert MediaAttribute to MediaMap
static DRing::MediaMap toMediaMap(const MediaAttribute& mediaAttr);
......@@ -85,6 +92,7 @@ public:
bool secure_ {true};
bool enabled_ {false};
std::string sourceUri_ {};
MediaSourceType sourceType_ {MediaSourceType::NONE};
std::string label_ {};
bool onHold_ {false};
......@@ -101,6 +109,6 @@ public:
// with or without a media direction change (no re-invite).
// For instance, muting the audio can be done by disabling the
// audio input (capture) of the encoding session, resulting in
// sending an RTP packets without actual audio (silence).
// sending RTP packets without actual audio (silence).
};
} // namespace jami
......@@ -53,6 +53,13 @@ enum MediaType : unsigned {
MEDIA_ALL = MEDIA_AUDIO | MEDIA_VIDEO
};
enum MediaSourceType : unsigned {
NONE = 0,
CAPTURE_DEVICE, // Camera or microphone
DISPLAY, // Screen sharing
FILE // File streaming
};
enum class RateMode : unsigned { CRF_CONSTRAINED, CQ, CBR };
/*
......
......@@ -54,6 +54,7 @@
#include <chrono>
#include <libavutil/display.h>
#endif
#include "audio/ringbufferpool.h"
#include "jamidht/channeled_transport.h"
#include "errno.h"
......@@ -667,7 +668,8 @@ SIPCall::setInviteSession(pjsip_inv_session* inviteSession)
// with pjsip.
if (PJ_SUCCESS != pjsip_inv_add_ref(inviteSession)) {
JAMI_WARN("[call:%s] trying to set invalid invite session [%p]",
getCallId().c_str(), inviteSession);
getCallId().c_str(),
inviteSession);
inviteSession_.reset(nullptr);
return;
}
......@@ -1759,6 +1761,10 @@ SIPCall::addMediaStream(const MediaAttribute& mediaAttr)
}
#endif
if (stream.mediaAttribute_->sourceType_ == MediaSourceType::NONE) {
stream.mediaAttribute_->sourceType_ = MediaSourceType::CAPTURE_DEVICE;
}
rtpStreams_.emplace_back(std::move(stream));
}
......@@ -1787,19 +1793,6 @@ SIPCall::initMediaStreams(const std::vector<MediaAttribute>& mediaAttrList)
return rtpStreams_.size();
}
bool
SIPCall::isAudioMuted() const
{
std::function<bool(const RtpStream& stream)> mutedCheck = [](auto const& stream) {
return (stream.mediaAttribute_->type_ == MediaType::MEDIA_AUDIO
and not stream.mediaAttribute_->muted_);
};
const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck);
return iter == rtpStreams_.end();
}
bool
SIPCall::hasVideo() const
{
......@@ -1817,32 +1810,17 @@ SIPCall::hasVideo() const
}
bool
SIPCall::isVideoMuted() const
SIPCall::isCaptureDeviceMuted(const MediaType& mediaType) const
{
#ifdef ENABLE_VIDEO
std::function<bool(const RtpStream& stream)> mutedCheck = [](auto const& stream) {
return (stream.mediaAttribute_->type_ == MediaType::MEDIA_VIDEO
// Return true only if all media of type 'mediaType' that use capture devices
// source, are muted.
std::function<bool(const RtpStream& stream)> mutedCheck = [&mediaType](auto const& stream) {
return (stream.mediaAttribute_->type_ == mediaType
and stream.mediaAttribute_->sourceType_ == MediaSourceType::CAPTURE_DEVICE
and not stream.mediaAttribute_->muted_);
};
const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), mutedCheck);
return iter == rtpStreams_.end();
#else
return true;
#endif
}
bool
SIPCall::isMediaTypeEnabled(MediaType type) const
{
#ifdef ENABLE_VIDEO
std::function<bool(const RtpStream& stream)> enabledCheck = [&type](auto const& stream) {
return (stream.mediaAttribute_->type_ == type and stream.mediaAttribute_->enabled_);
};
const auto iter = std::find_if(rtpStreams_.begin(), rtpStreams_.end(), enabledCheck);
return iter != rtpStreams_.end();
#else
return false;
#endif
}
void
......@@ -2685,6 +2663,12 @@ SIPCall::enterConference(const std::string& confId)
void
SIPCall::exitConference()
{
auto const& audioRtp = getAudioRtp();
if (audioRtp && !isCaptureDeviceMuted(MediaType::MEDIA_AUDIO)) {
auto& rbPool = Manager::instance().getRingBufferPool();
rbPool.bindCallID(getCallId(), RingBufferPool::DEFAULT_ID);
rbPool.flush(RingBufferPool::DEFAULT_ID);
}
#ifdef ENABLE_VIDEO
auto const& videoRtp = getVideoRtp();
if (videoRtp)
......
......@@ -154,8 +154,8 @@ public:
std::shared_ptr<Observable<std::shared_ptr<MediaFrame>>> getReceiveVideoFrameActiveWriter()
override;
bool hasVideo() const override;
bool isAudioMuted() const override;
bool isVideoMuted() const override;
bool isCaptureDeviceMuted(const MediaType& mediaType) const override;
bool isSrtpEnabled() const { return srtpEnabled_; }
// End of override of Call class
// Override of Recordable class
......@@ -258,8 +258,6 @@ public:
// Get the list of current RTP sessions
std::vector<std::shared_ptr<RtpSession>> getRtpSessionList() const;
bool isSrtpEnabled() const { return srtpEnabled_; }
void generateMediaPorts();
void openPortsUPnP();
......@@ -281,7 +279,6 @@ public:
void setMute(bool state);
bool isMediaTypeEnabled(MediaType type) const;
void setInviteSession(pjsip_inv_session* inviteSession = nullptr);
std::unique_ptr<pjsip_inv_session, InvSessionDeleter> inviteSession_;
......
......@@ -403,7 +403,7 @@ transaction_request_cb(pjsip_rx_data* rdata)
// Build the initial media using the remote offer.
auto localMediaList = Sdp::getMediaAttributeListFromSdp(r_sdp);
// To enable video, it must enabled in the remote and locally (i.e. in the account)
// To enable video, it must be enabled in the remote and locally (i.e. in the account)
for (auto& media : localMediaList) {
if (media.type_ == MediaType::MEDIA_VIDEO) {
media.enabled_ &= account->isVideoEnabled();
......