Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 343 additions & 10 deletions gui_client/BrowserVidPlayer.cpp

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions gui_client/BrowserVidPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class BrowserVidPlayer : public RefCounted
bool using_iframe;

bool previous_is_visible;

bool watch_party_state_requested;
bool joined_watch_party;
double last_watch_party_sync_check_time;
};


Expand Down
32 changes: 32 additions & 0 deletions gui_client/ClientThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,38 @@ void ClientThread::readAndHandleMessage(const uint32 peer_protocol_version)
}
break;
}
case Protocol::VideoWatchPartyState:
{
const UID object_uid = readUIDFromStream(msg_buffer);
const bool active = msg_buffer.readUInt32() != 0;
const uint32 owner_user_id = msg_buffer.readUInt32();
const double start_global_time = msg_buffer.readDouble();
const double start_video_time = msg_buffer.readDouble();
bool is_playing = true;
try
{
is_playing = (msg_buffer.readUInt32() != 0); // Optional, for backward compatibility with older server packets.
}
catch(glare::Exception&)
{}

{
Lock lock(world_state->mutex);
auto res = world_state->objects.find(object_uid);
if(res != world_state->objects.end())
{
WorldObject* ob = res.getValue().ptr();
ob->video_watch_party_active = active;
ob->video_watch_party_owner_user_id = owner_user_id;
ob->video_watch_party_start_global_time = start_global_time;
ob->video_watch_party_start_video_time = start_video_time;
ob->video_watch_party_is_playing = is_playing;
ob->from_remote_video_watch_party_dirty = true;
world_state->dirty_from_remote_objects.insert(ob);
}
}
break;
}
case Protocol::ObjectPhysicsOwnershipTaken:
{
const UID object_uid = readUIDFromStream(msg_buffer);
Expand Down
61 changes: 61 additions & 0 deletions gui_client/EmbeddedBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,54 @@ class EmbeddedBrowserCefClient : public CefClient, public CefRequestHandler, pub
bool is_redirect) override

{
const std::string req_url = request->GetURL().ToString();
if(hasPrefix(req_url, "https://localdomain/watchparty"))
{
try
{
const URL parsed_url = URL::parseURL(req_url);
const std::map<std::string, std::string> params = URL::parseQuery(parsed_url.query);

GUIClient* gui_client = NULL;
WorldObject* world_ob = NULL;
{
Lock lock(mutex);
gui_client = m_gui_client;
world_ob = m_ob;
}

if(gui_client && world_ob)
{
const auto action_it = params.find("action");
if(action_it != params.end())
{
if(action_it->second == "start" || action_it->second == "update")
{
double start_video_time = 0.0;
const auto t_it = params.find("t");
if(t_it != params.end())
start_video_time = stringToDouble(t_it->second);

bool is_playing = true;
const auto p_it = params.find("p");
if(p_it != params.end())
is_playing = (p_it->second == "1");

gui_client->startVideoWatchParty(world_ob->uid, start_video_time, is_playing);
}
else if(action_it->second == "join")
{
gui_client->requestVideoWatchPartyState(world_ob->uid);
}
}
}
}
catch(glare::Exception&)
{}

return true;
}

/*std::string frame_name = frame->GetName();
std::string frame_url = frame->GetURL();

Expand Down Expand Up @@ -1227,6 +1275,19 @@ void EmbeddedBrowser::navigate(const std::string& new_URL)
}


void EmbeddedBrowser::executeJavaScript(const std::string& script)
{
#if CEF_SUPPORT
if(embedded_cef_browser && embedded_cef_browser->cef_browser)
{
CefRefPtr<CefFrame> frame = embedded_cef_browser->cef_browser->GetMainFrame();
if(frame)
frame->ExecuteJavaScript(script, URL, 0);
}
#endif
}


void EmbeddedBrowser::browserBecameVisible()
{
#if CEF_SUPPORT
Expand Down
1 change: 1 addition & 0 deletions gui_client/EmbeddedBrowser.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class EmbeddedBrowser : public RefCounted

void updateRootPage(const std::string& root_page);
void navigate(const std::string& URL);
void executeJavaScript(const std::string& script);

void browserBecameVisible();

Expand Down
28 changes: 28 additions & 0 deletions gui_client/GUIClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6927,6 +6927,10 @@ void GUIClient::timerEvent(const MouseCursorState& mouse_cursor_state)

ob->from_remote_physics_ownership_dirty = false;
}
else if(ob->from_remote_video_watch_party_dirty)
{
ob->from_remote_video_watch_party_dirty = false;
}

if(ob->from_remote_transform_dirty)
{
Expand Down Expand Up @@ -12528,6 +12532,30 @@ void GUIClient::sendLightmapNeededFlagsSlot()
}


void GUIClient::requestVideoWatchPartyState(const UID& object_uid)
{
if(this->connection_state != ServerConnectionState_Connected || this->client_thread.isNull())
return;

MessageUtils::initPacket(scratch_packet, Protocol::VideoWatchPartyQueryState);
writeToStream(object_uid, scratch_packet);
enqueueMessageToSend(*this->client_thread, scratch_packet);
}


void GUIClient::startVideoWatchParty(const UID& object_uid, double start_video_time, bool is_playing)
{
if(this->connection_state != ServerConnectionState_Connected || this->client_thread.isNull())
return;

MessageUtils::initPacket(scratch_packet, Protocol::VideoWatchPartyStart);
writeToStream(object_uid, scratch_packet);
scratch_packet.writeDouble(start_video_time);
scratch_packet.writeUInt32(is_playing ? 1u : 0u);
enqueueMessageToSend(*this->client_thread, scratch_packet);
}


static std::string makeURL(const std::string& server_hostname, const std::string& server_worldname, const Vec3d& pos, double heading_deg)
{
// Use doubleToStringNDecimalPlaces instead of doubleToStringMaxNDecimalPlaces, as the latter is distracting due to flickering URL length when moving.
Expand Down
2 changes: 2 additions & 0 deletions gui_client/GUIClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ class GUIClient : public ObLoadingCallbacks, public PrintOutput, public PhysicsW
void viewportResized(int w, int h);
void updateGroundPlane();
void sendLightmapNeededFlagsSlot();
void requestVideoWatchPartyState(const UID& object_uid);
void startVideoWatchParty(const UID& object_uid, double start_video_time, bool is_playing);
void useActionTriggered(bool use_mouse_cursor); // if use_mouse_cursor is false, use crosshair as cursor instead.
void loginButtonClicked();
void signupButtonClicked();
Expand Down
19 changes: 19 additions & 0 deletions gui_client/ObjectEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ ObjectEditor::ObjectEditor(QWidget *parent)
connect(this->videoAutoplayCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(objectChanged()));
connect(this->videoLoopCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(objectChanged()));
connect(this->videoMutedCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(objectChanged()));
connect(this->videoSyncCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(objectChanged()));

connect(this->videoURLFileSelectWidget, SIGNAL(filenameChanged(QString&)), this, SIGNAL(objectChanged()));
connect(this->videoURLFileSelectWidget, SIGNAL(filenameChanged(QString&)), this, SLOT(videoURLChanged(QString&)));
connect(this->videoVolumeDoubleSpinBox, SIGNAL(valueChanged(double)), this, SIGNAL(objectChanged()));

connect(this->audioAutoplayCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(objectChanged()));
Expand Down Expand Up @@ -257,11 +259,16 @@ void ObjectEditor::setFromObject(const WorldObject& ob, int selected_mat_index_,
SignalBlocker::setChecked(this->videoAutoplayCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::VIDEO_AUTOPLAY));
SignalBlocker::setChecked(this->videoLoopCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::VIDEO_LOOP));
SignalBlocker::setChecked(this->videoMutedCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::VIDEO_MUTED));
SignalBlocker::setChecked(this->videoSyncCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::VIDEO_SYNC_TO_CLOCK));

SignalBlocker::setChecked(this->audioAutoplayCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::AUDIO_AUTOPLAY));
SignalBlocker::setChecked(this->audioLoopCheckBox, BitUtils::isBitSet(ob.flags, WorldObject::AUDIO_LOOP));

this->videoURLFileSelectWidget->setFilename(QtUtils::toQString((!ob.materials.empty()) ? ob.materials[0]->emission_texture_url : ""));
{
QString url = this->videoURLFileSelectWidget->filename();
videoURLChanged(url);
}

SignalBlocker::setValue(videoVolumeDoubleSpinBox, ob.audio_volume);

Expand Down Expand Up @@ -533,6 +540,7 @@ void ObjectEditor::toObject(WorldObject& ob_out)
BitUtils::setOrZeroBit(ob_out.flags, WorldObject::VIDEO_AUTOPLAY, this->videoAutoplayCheckBox->isChecked());
BitUtils::setOrZeroBit(ob_out.flags, WorldObject::VIDEO_LOOP, this->videoLoopCheckBox ->isChecked());
BitUtils::setOrZeroBit(ob_out.flags, WorldObject::VIDEO_MUTED, this->videoMutedCheckBox ->isChecked());
BitUtils::setOrZeroBit(ob_out.flags, WorldObject::VIDEO_SYNC_TO_CLOCK, this->videoSyncCheckBox->isChecked());

BitUtils::setOrZeroBit(ob_out.flags, WorldObject::AUDIO_AUTOPLAY, this->audioAutoplayCheckBox->isChecked());
BitUtils::setOrZeroBit(ob_out.flags, WorldObject::AUDIO_LOOP, this->audioLoopCheckBox ->isChecked());
Expand Down Expand Up @@ -820,6 +828,17 @@ void ObjectEditor::targetURLChanged()
}


void ObjectEditor::videoURLChanged(QString&)
{
const std::string lower_url = ::toLowerCase(QtUtils::toIndString(this->videoURLFileSelectWidget->filename()));
const bool is_youtube_url = StringUtils::containsString(lower_url, "youtube.com") || StringUtils::containsString(lower_url, "youtu.be");

this->videoSyncCheckBox->setEnabled(is_youtube_url);
if(!is_youtube_url)
SignalBlocker::setChecked(this->videoSyncCheckBox, false);
}


void ObjectEditor::scriptTextEditChanged()
{
edit_timer->start();
Expand Down
1 change: 1 addition & 0 deletions gui_client/ObjectEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private slots:
void on_materialComboBox_currentIndexChanged(int index);
void on_newMaterialPushButton_clicked(bool checked);
void targetURLChanged();
void videoURLChanged(QString&);
void scriptTextEditChanged();
void scriptChangedFromEditor();
void on_editScriptPushButton_clicked(bool checked);
Expand Down
10 changes: 10 additions & 0 deletions gui_client/ObjectEditor.ui
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,16 @@
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="videoSyncCheckBox">
<property name="toolTip">
<string>For looped YouTube videos, periodically sync playback time across viewers.</string>
</property>
<property name="text">
<string>Sync (YouTube)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
Expand Down
13 changes: 13 additions & 0 deletions server/ServerWorldState.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ class ServerWorldState : public ThreadSafeRefCounted
public:
ServerWorldState() : db_dirty(false) {}

struct VideoWatchPartyState
{
VideoWatchPartyState() : active(false), owner_user_id(std::numeric_limits<uint32>::max()), start_global_time(0), start_video_time(0), is_playing(true) {}
bool active;
uint32 owner_user_id;
double start_global_time;
double start_video_time;
bool is_playing;
};

void addParcelAsDBDirty (const ParcelRef parcel, WorldStateLock& /*world_state_lock*/) { db_dirty_parcels.insert(parcel); }
void addWorldObjectAsDBDirty(const WorldObjectRef ob, WorldStateLock& /*world_state_lock*/) { db_dirty_world_objects.insert(ob); }
void addLODChunkAsDBDirty (const LODChunkRef ob, WorldStateLock& /*world_state_lock*/) { db_dirty_lod_chunks.insert(ob); }
Expand Down Expand Up @@ -248,6 +258,7 @@ class ServerWorldState : public ThreadSafeRefCounted
ParcelMapType& getParcels(WorldStateLock& /*world_state_lock*/) { return parcels; }
LODChunkMapType& getLODChunks(WorldStateLock& /*world_state_lock*/) { return lod_chunks; }
ChatBotMapType& getChatBots(WorldStateLock& /*world_state_lock*/) { return chatbots; }
std::map<UID, VideoWatchPartyState>& getVideoWatchParties(WorldStateLock& /*world_state_lock*/) { return video_watch_parties; }

ParcelMapType parcels; // TODO: make private. Lots of compile errors to fix when doing so.

Expand All @@ -258,6 +269,8 @@ class ServerWorldState : public ThreadSafeRefCounted
std::unordered_set<ChatBotRef, ChatBotRefHash>& getDBDirtyChatBots(WorldStateLock& /*world_state_lock*/) { return db_dirty_chatbots; }

AvatarRef createAndInsertAvatarForChatBot(ServerAllWorldsState* all_world_state, const ChatBot* chatbot, WorldStateLock& /*world_state_lock*/);

std::map<UID, VideoWatchPartyState> video_watch_parties;
private:
ObjectMapType objects;
DirtyFromRemoteObjectSetType dirty_from_remote_objects; // TODO: could just use vector for this, and avoid duplicates by checking object dirty flag.
Expand Down
87 changes: 87 additions & 0 deletions server/WorkerThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,92 @@ void WorkerThread::doRun()
}
}
}
break;
}
case Protocol::VideoWatchPartyStart:
{
const UID object_uid = readUIDFromStream(msg_buffer);
const double requested_start_video_time = msg_buffer.readDouble();
bool requested_is_playing = true;
try
{
requested_is_playing = (msg_buffer.readUInt32() != 0); // Optional, for backward compatibility with older client packets.
}
catch(glare::Exception&)
{}

ServerWorldState::VideoWatchPartyState state_to_send;
bool should_send = false;

{
WorldStateLock lock(world_state->mutex);
auto ob_res = cur_world_state->getObjects(lock).find(object_uid);
if(ob_res != cur_world_state->getObjects(lock).end())
{
WorldObject* ob = ob_res->second.getPointer();
if(ob->object_type == WorldObject::ObjectType_Video && BitUtils::isBitSet(ob->flags, WorldObject::VIDEO_SYNC_TO_CLOCK))
{
auto& state = cur_world_state->getVideoWatchParties(lock)[object_uid];
const uint32 request_user_id = client_user_id.valid() ? client_user_id.value() : std::numeric_limits<uint32>::max();

if(!state.active)
{
state.active = true;
state.owner_user_id = request_user_id;
state.start_global_time = server->getCurrentGlobalTime();
state.start_video_time = myMax(0.0, requested_start_video_time);
state.is_playing = requested_is_playing;
}
else if(state.owner_user_id == request_user_id)
{
state.start_global_time = server->getCurrentGlobalTime();
state.start_video_time = myMax(0.0, requested_start_video_time);
state.is_playing = requested_is_playing;
}

state_to_send = state;
should_send = true;
}
}
}

if(should_send)
{
MessageUtils::initPacket(scratch_packet, Protocol::VideoWatchPartyState);
writeToStream(object_uid, scratch_packet);
scratch_packet.writeUInt32(state_to_send.active ? 1u : 0u);
scratch_packet.writeUInt32(state_to_send.owner_user_id);
scratch_packet.writeDouble(state_to_send.start_global_time);
scratch_packet.writeDouble(state_to_send.start_video_time);
scratch_packet.writeUInt32(state_to_send.is_playing ? 1u : 0u);
MessageUtils::updatePacketLengthField(scratch_packet);
enqueuePacketToBroadcast(scratch_packet);
}

break;
}
case Protocol::VideoWatchPartyQueryState:
{
const UID object_uid = readUIDFromStream(msg_buffer);

ServerWorldState::VideoWatchPartyState state_to_send;
{
WorldStateLock lock(world_state->mutex);
auto res = cur_world_state->getVideoWatchParties(lock).find(object_uid);
if(res != cur_world_state->getVideoWatchParties(lock).end())
state_to_send = res->second;
}

MessageUtils::initPacket(scratch_packet, Protocol::VideoWatchPartyState);
writeToStream(object_uid, scratch_packet);
scratch_packet.writeUInt32(state_to_send.active ? 1u : 0u);
scratch_packet.writeUInt32(state_to_send.owner_user_id);
scratch_packet.writeDouble(state_to_send.start_global_time);
scratch_packet.writeDouble(state_to_send.start_video_time);
scratch_packet.writeUInt32(state_to_send.is_playing ? 1u : 0u);
MessageUtils::updatePacketLengthField(scratch_packet);
enqueueDataToSend(scratch_packet);

break;
}
case Protocol::ObjectPhysicsOwnershipTaken:
Expand Down Expand Up @@ -2342,6 +2428,7 @@ void WorkerThread::doRun()
// Mark object as dead
ob->state = WorldObject::State_Dead;
ob->from_remote_other_dirty = true;
cur_world_state->getVideoWatchParties(lock).erase(object_uid);
cur_world_state->addWorldObjectAsDBDirty(ob, lock);
cur_world_state->getDirtyFromRemoteObjects(lock).insert(ob);

Expand Down
Loading