blob: ccad9f90c1268c1ec318f11960fc845db897dad0 [file] [log] [blame]
/*
* Copyright (C) 2015-2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebMediaSessionManager.h"
#if ENABLE(WIRELESS_PLAYBACK_TARGET) && !PLATFORM(IOS_FAMILY)
#include "FloatRect.h"
#include "Logging.h"
#include "MediaPlaybackTargetPickerMock.h"
#include "WebMediaSessionManagerClient.h"
#include <wtf/Algorithms.h>
#include <wtf/Logger.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
static const Seconds taskDelayInterval { 100_ms };
#undef LOGIDENTIFIER
#define LOGIDENTIFIER __func__
#undef ALWAYS_LOG
#define ALWAYS_LOG logger().logAlways
struct ClientState {
WTF_MAKE_STRUCT_FAST_ALLOCATED;
explicit ClientState(WebMediaSessionManagerClient& client, uint64_t contextId)
: client(client)
, contextId(contextId)
{
}
bool operator == (ClientState const& other) const
{
return contextId == other.contextId && &client == &other.client;
}
WebMediaSessionManagerClient& client;
uint64_t contextId { 0 };
WebCore::MediaProducer::MediaStateFlags flags { WebCore::MediaProducer::IsNotPlaying };
bool requestedPicker { false };
bool previouslyRequestedPicker { false };
bool configurationRequired { true };
bool playedToEnd { false };
};
static bool flagsAreSet(MediaProducer::MediaStateFlags value, unsigned flags)
{
return value & flags;
}
String mediaProducerStateString(MediaProducer::MediaStateFlags flags)
{
StringBuilder string;
if (flags & MediaProducer::IsPlayingAudio)
string.append("IsPlayingAudio+");
if (flags & MediaProducer::IsPlayingVideo)
string.append("IsPlayingVideo+");
if (flags & MediaProducer::IsPlayingToExternalDevice)
string.append("IsPlayingToExternalDevice+");
if (flags & MediaProducer::HasPlaybackTargetAvailabilityListener)
string.append("HasTargetAvailabilityListener+");
if (flags & MediaProducer::RequiresPlaybackTargetMonitoring)
string.append("RequiresTargetMonitoring+");
if (flags & MediaProducer::ExternalDeviceAutoPlayCandidate)
string.append("ExternalDeviceAutoPlayCandidate+");
if (flags & MediaProducer::DidPlayToEnd)
string.append("DidPlayToEnd+");
if (flags & MediaProducer::HasAudioOrVideo)
string.append("HasAudioOrVideo+");
if (string.isEmpty())
string.append("IsNotPlaying");
else
string.resize(string.length() - 1);
return makeString(" { ", string.toString(), " }");
}
class WebMediaSessionLogger {
WTF_MAKE_NONCOPYABLE(WebMediaSessionLogger);
WTF_MAKE_FAST_ALLOCATED;
public:
static std::unique_ptr<WebMediaSessionLogger> create(WebMediaSessionManager& manager)
{
return makeUnique<WebMediaSessionLogger>(manager);
}
template<typename... Arguments>
inline void logAlways(const char* methodName, ClientState* state, const Arguments&... arguments) const
{
if (!state->client.alwaysOnLoggingAllowed())
return;
m_logger->logAlways(LogMedia, makeString("WebMediaSessionManager::", methodName, ' '), state->contextId, state->flags, arguments...);
}
template<typename... Arguments>
inline void logAlways(const char* methodName, const Arguments&... arguments) const
{
if (!m_manager.alwaysOnLoggingAllowed())
return;
m_logger->logAlways(LogMedia, makeString("WebMediaSessionManager::", methodName, ' '), arguments...);
}
private:
friend std::unique_ptr<WebMediaSessionLogger> std::make_unique<WebMediaSessionLogger>(WebMediaSessionManager&);
WebMediaSessionLogger(WebMediaSessionManager& manager)
: m_manager(manager)
, m_logger(Logger::create(this))
{
}
WebMediaSessionManager& m_manager;
Ref<Logger> m_logger;
};
WebMediaSessionLogger& WebMediaSessionManager::logger()
{
if (!m_logger)
m_logger = WebMediaSessionLogger::create(*this);
return *m_logger;
}
bool WebMediaSessionManager::alwaysOnLoggingAllowed() const
{
return allOf(m_clientState, [] (auto& state) {
return state->client.alwaysOnLoggingAllowed();
});
}
void WebMediaSessionManager::setMockMediaPlaybackTargetPickerEnabled(bool enabled)
{
if (m_mockPickerEnabled == enabled)
return;
ALWAYS_LOG(LOGIDENTIFIER);
m_mockPickerEnabled = enabled;
}
void WebMediaSessionManager::setMockMediaPlaybackTargetPickerState(const String& name, MediaPlaybackTargetContext::State state)
{
ALWAYS_LOG(LOGIDENTIFIER);
mockPicker().setState(name, state);
}
void WebMediaSessionManager::mockMediaPlaybackTargetPickerDismissPopup()
{
ALWAYS_LOG(LOGIDENTIFIER);
mockPicker().dismissPopup();
}
MediaPlaybackTargetPickerMock& WebMediaSessionManager::mockPicker()
{
if (!m_pickerOverride)
m_pickerOverride = makeUnique<MediaPlaybackTargetPickerMock>(*this);
return *m_pickerOverride.get();
}
WebCore::MediaPlaybackTargetPicker& WebMediaSessionManager::targetPicker()
{
if (m_mockPickerEnabled)
return mockPicker();
return platformPicker();
}
WebMediaSessionManager::WebMediaSessionManager()
: m_taskTimer(RunLoop::current(), this, &WebMediaSessionManager::taskTimerFired)
, m_watchdogTimer(RunLoop::current(), this, &WebMediaSessionManager::watchdogTimerFired)
{
}
WebMediaSessionManager::~WebMediaSessionManager() = default;
uint64_t WebMediaSessionManager::addPlaybackTargetPickerClient(WebMediaSessionManagerClient& client, uint64_t contextId)
{
size_t index = find(&client, contextId);
ASSERT(index == notFound);
if (index != notFound)
return 0;
ALWAYS_LOG(LOGIDENTIFIER, contextId);
m_clientState.append(makeUnique<ClientState>(client, contextId));
if (m_externalOutputDeviceAvailable || m_playbackTarget)
scheduleDelayedTask(InitialConfigurationTask | TargetClientsConfigurationTask);
return contextId;
}
void WebMediaSessionManager::removePlaybackTargetPickerClient(WebMediaSessionManagerClient& client, uint64_t contextId)
{
size_t index = find(&client, contextId);
ASSERT(index != notFound);
if (index == notFound)
return;
ALWAYS_LOG(LOGIDENTIFIER, m_clientState[index].get());
m_clientState.remove(index);
scheduleDelayedTask(TargetMonitoringConfigurationTask | TargetClientsConfigurationTask);
}
void WebMediaSessionManager::removeAllPlaybackTargetPickerClients(WebMediaSessionManagerClient& client)
{
if (m_clientState.isEmpty())
return;
for (size_t i = m_clientState.size(); i > 0; --i) {
if (&m_clientState[i - 1]->client == &client) {
ALWAYS_LOG(LOGIDENTIFIER, m_clientState[i - 1].get());
m_clientState.remove(i - 1);
}
}
scheduleDelayedTask(TargetMonitoringConfigurationTask | TargetClientsConfigurationTask);
}
void WebMediaSessionManager::showPlaybackTargetPicker(WebMediaSessionManagerClient& client, uint64_t contextId, const IntRect& rect, bool, bool useDarkAppearance)
{
size_t index = find(&client, contextId);
ASSERT(index != notFound);
if (index == notFound)
return;
auto& clientRequestingPicker = m_clientState[index];
for (auto& state : m_clientState) {
state->requestedPicker = state == clientRequestingPicker;
state->previouslyRequestedPicker = state == clientRequestingPicker;
}
ALWAYS_LOG(LOGIDENTIFIER, m_clientState[index].get());
bool hasActiveRoute = flagsAreSet(m_clientState[index]->flags, MediaProducer::IsPlayingToExternalDevice);
targetPicker().showPlaybackTargetPicker(FloatRect(rect), hasActiveRoute, useDarkAppearance);
}
void WebMediaSessionManager::clientStateDidChange(WebMediaSessionManagerClient& client, uint64_t contextId, MediaProducer::MediaStateFlags newFlags)
{
size_t index = find(&client, contextId);
ASSERT(index != notFound);
if (index == notFound)
return;
auto& changedClientState = m_clientState[index];
MediaProducer::MediaStateFlags oldFlags = changedClientState->flags;
if (newFlags == oldFlags)
return;
ALWAYS_LOG(LOGIDENTIFIER, m_clientState[index].get(), "new flags = ", newFlags);
changedClientState->flags = newFlags;
MediaProducer::MediaStateFlags updateConfigurationFlags = MediaProducer::RequiresPlaybackTargetMonitoring | MediaProducer::HasPlaybackTargetAvailabilityListener | MediaProducer::HasAudioOrVideo;
if ((oldFlags & updateConfigurationFlags) != (newFlags & updateConfigurationFlags))
scheduleDelayedTask(TargetMonitoringConfigurationTask);
MediaProducer::MediaStateFlags playingToTargetFlags = MediaProducer::IsPlayingToExternalDevice | MediaProducer::IsPlayingVideo;
if ((oldFlags & playingToTargetFlags) != (newFlags & playingToTargetFlags)) {
if (flagsAreSet(oldFlags, MediaProducer::IsPlayingVideo) && !flagsAreSet(newFlags, MediaProducer::IsPlayingVideo) && flagsAreSet(newFlags, MediaProducer::DidPlayToEnd))
changedClientState->playedToEnd = true;
scheduleDelayedTask(WatchdogTimerConfigurationTask);
}
if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute() || !flagsAreSet(newFlags, MediaProducer::ExternalDeviceAutoPlayCandidate))
return;
// Do not interrupt another element already playing to a device.
for (auto& state : m_clientState) {
if (state == changedClientState)
continue;
if (flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice) && flagsAreSet(state->flags, MediaProducer::IsPlayingVideo)) {
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " returning early");
return;
}
}
// Do not begin playing to the device unless playback has just started.
if (!flagsAreSet(newFlags, MediaProducer::IsPlayingVideo) || flagsAreSet(oldFlags, MediaProducer::IsPlayingVideo)) {
ALWAYS_LOG(LOGIDENTIFIER, "returning early, playback didn't just start");
return;
}
for (auto& state : m_clientState) {
if (state == changedClientState)
continue;
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " calling setShouldPlayToPlaybackTarget(false)");
state->client.setShouldPlayToPlaybackTarget(state->contextId, false);
}
ALWAYS_LOG(LOGIDENTIFIER, changedClientState.get(), " calling setShouldPlayToPlaybackTarget(true)");
changedClientState->client.setShouldPlayToPlaybackTarget(changedClientState->contextId, true);
if (index && m_clientState.size() > 1)
std::swap(m_clientState.at(index), m_clientState.at(0));
}
void WebMediaSessionManager::setPlaybackTarget(Ref<MediaPlaybackTarget>&& target)
{
ALWAYS_LOG(LOGIDENTIFIER, "has active route = ", target->hasActiveRoute());
m_playbackTarget = WTFMove(target);
m_targetChanged = true;
scheduleDelayedTask(TargetClientsConfigurationTask);
}
void WebMediaSessionManager::externalOutputDeviceAvailableDidChange(bool available)
{
ALWAYS_LOG(LOGIDENTIFIER, available);
m_externalOutputDeviceAvailable = available;
for (auto& state : m_clientState)
state->client.externalOutputDeviceAvailableDidChange(state->contextId, available);
}
void WebMediaSessionManager::playbackTargetPickerWasDismissed()
{
ALWAYS_LOG(LOGIDENTIFIER);
m_playbackTargetPickerDismissed = true;
scheduleDelayedTask(TargetClientsConfigurationTask);
}
void WebMediaSessionManager::configureNewClients()
{
for (auto& state : m_clientState) {
if (!state->configurationRequired)
continue;
state->configurationRequired = false;
if (m_externalOutputDeviceAvailable)
state->client.externalOutputDeviceAvailableDidChange(state->contextId, true);
if (m_playbackTarget)
state->client.setPlaybackTarget(state->contextId, *m_playbackTarget.copyRef());
}
}
void WebMediaSessionManager::configurePlaybackTargetClients()
{
if (m_clientState.isEmpty())
return;
size_t indexOfClientThatRequestedPicker = notFound;
size_t indexOfLastClientToRequestPicker = notFound;
size_t indexOfClientWillPlayToTarget = notFound;
bool haveActiveRoute = m_playbackTarget && m_playbackTarget->hasActiveRoute();
for (size_t i = 0; i < m_clientState.size(); ++i) {
auto& state = m_clientState[i];
ALWAYS_LOG(LOGIDENTIFIER, state.get(), ", requestedPicker = ", state->requestedPicker);
if ((m_targetChanged || m_playbackTargetPickerDismissed) && state->requestedPicker)
indexOfClientThatRequestedPicker = i;
if (indexOfClientWillPlayToTarget == notFound && flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice))
indexOfClientWillPlayToTarget = i;
if (indexOfClientWillPlayToTarget == notFound && haveActiveRoute && state->previouslyRequestedPicker)
indexOfLastClientToRequestPicker = i;
}
if (indexOfClientThatRequestedPicker != notFound)
indexOfClientWillPlayToTarget = indexOfClientThatRequestedPicker;
if (indexOfClientWillPlayToTarget == notFound && indexOfLastClientToRequestPicker != notFound)
indexOfClientWillPlayToTarget = indexOfLastClientToRequestPicker;
if (indexOfClientWillPlayToTarget == notFound && haveActiveRoute && flagsAreSet(m_clientState[0]->flags, MediaProducer::ExternalDeviceAutoPlayCandidate) && !flagsAreSet(m_clientState[0]->flags, MediaProducer::IsPlayingVideo))
indexOfClientWillPlayToTarget = 0;
for (size_t i = 0; i < m_clientState.size(); ++i) {
auto& state = m_clientState[i];
if (m_playbackTarget)
state->client.setPlaybackTarget(state->contextId, *m_playbackTarget.copyRef());
if (i != indexOfClientWillPlayToTarget || !haveActiveRoute) {
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " calling setShouldPlayToPlaybackTarget(false)");
state->client.setShouldPlayToPlaybackTarget(state->contextId, false);
}
if (state->requestedPicker && m_playbackTargetPickerDismissed) {
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " calling playbackTargetPickerWasDismissed");
state->client.playbackTargetPickerWasDismissed(state->contextId);
}
state->configurationRequired = false;
if (m_targetChanged || m_playbackTargetPickerDismissed)
state->requestedPicker = false;
}
if (haveActiveRoute && indexOfClientWillPlayToTarget != notFound) {
auto& state = m_clientState[indexOfClientWillPlayToTarget];
if (!flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice)) {
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " calling setShouldPlayToPlaybackTarget(true)");
state->client.setShouldPlayToPlaybackTarget(state->contextId, true);
}
}
m_targetChanged = false;
configureWatchdogTimer();
}
void WebMediaSessionManager::configurePlaybackTargetMonitoring()
{
bool monitoringRequired = false;
bool hasAvailabilityListener = false;
bool haveClientWithMedia = false;
for (auto& state : m_clientState) {
ALWAYS_LOG(LOGIDENTIFIER, state.get());
if (state->flags & MediaProducer::RequiresPlaybackTargetMonitoring) {
monitoringRequired = true;
break;
}
if (state->flags & MediaProducer::HasPlaybackTargetAvailabilityListener)
hasAvailabilityListener = true;
if (state->flags & MediaProducer::HasAudioOrVideo)
haveClientWithMedia = true;
}
if (monitoringRequired || (hasAvailabilityListener && haveClientWithMedia)) {
ALWAYS_LOG(LOGIDENTIFIER, "starting monitoring");
targetPicker().startingMonitoringPlaybackTargets();
} else {
ALWAYS_LOG(LOGIDENTIFIER, "stopping monitoring");
targetPicker().stopMonitoringPlaybackTargets();
}
}
void WebMediaSessionManager::scheduleDelayedTask(ConfigurationTasks tasks)
{
m_taskFlags |= tasks;
m_taskTimer.startOneShot(taskDelayInterval);
}
void WebMediaSessionManager::taskTimerFired()
{
if (m_taskFlags & InitialConfigurationTask)
configureNewClients();
if (m_taskFlags & TargetClientsConfigurationTask)
configurePlaybackTargetClients();
if (m_taskFlags & TargetMonitoringConfigurationTask)
configurePlaybackTargetMonitoring();
if (m_taskFlags & WatchdogTimerConfigurationTask)
configureWatchdogTimer();
m_taskFlags = NoTask;
}
size_t WebMediaSessionManager::find(WebMediaSessionManagerClient* client, uint64_t contextId)
{
for (size_t i = 0; i < m_clientState.size(); ++i) {
if (m_clientState[i]->contextId == contextId && &m_clientState[i]->client == client)
return i;
}
return notFound;
}
void WebMediaSessionManager::configureWatchdogTimer()
{
static const Seconds watchdogTimerIntervalAfterPausing { 1_h };
static const Seconds watchdogTimerIntervalAfterPlayingToEnd { 8_min };
if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute()) {
if (m_watchdogTimer.isActive()) {
ALWAYS_LOG(LOGIDENTIFIER, "stopping timer");
m_currentWatchdogInterval = { };
m_watchdogTimer.stop();
}
return;
}
bool stopTimer = false;
bool didPlayToEnd = false;
for (auto& state : m_clientState) {
ALWAYS_LOG(LOGIDENTIFIER, state.get(), " playedToEnd = ", state->playedToEnd);
if (flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice) && flagsAreSet(state->flags, MediaProducer::IsPlayingVideo))
stopTimer = true;
if (state->playedToEnd)
didPlayToEnd = true;
state->playedToEnd = false;
}
if (stopTimer) {
ALWAYS_LOG(LOGIDENTIFIER, "stopping timer");
m_currentWatchdogInterval = { };
m_watchdogTimer.stop();
} else {
Seconds interval = didPlayToEnd ? watchdogTimerIntervalAfterPlayingToEnd : watchdogTimerIntervalAfterPausing;
if (interval != m_currentWatchdogInterval || !m_watchdogTimer.isActive()) {
m_watchdogTimer.startOneShot(interval);
}
ALWAYS_LOG(LOGIDENTIFIER, "timer scheduled for ", interval.value(), " seconds");
m_currentWatchdogInterval = interval;
}
}
void WebMediaSessionManager::watchdogTimerFired()
{
if (!m_playbackTarget)
return;
ALWAYS_LOG(LOGIDENTIFIER);
targetPicker().invalidatePlaybackTargets();
}
} // namespace WebCore
#endif // ENABLE(WIRELESS_PLAYBACK_TARGET) && !PLATFORM(IOS_FAMILY)