diff --git a/src/call.cpp b/src/call.cpp index f2826f141ea87adf44bb4d2bfd431ef616c881be..a151c0bff9c567922795add5a10992b65c442736 100644 --- a/src/call.cpp +++ b/src/call.cpp @@ -692,6 +692,8 @@ Call::setConferenceInfo(const std::string& msg) if (confID_.empty()) { // confID_ empty -> participant set confInfo with the received one confInfo_ = std::move(newInfo); + // Create sink for each participant + createSinks(confInfo_); // Inform client that layout has changed jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>( id_, confInfo_.toVectorMapStringString()); diff --git a/src/call.h b/src/call.h index 890a146e139551cfed21ebca55ee5986aa542969..972860cd69d401fa99a09087754888125ad77e59 100644 --- a/src/call.h +++ b/src/call.h @@ -386,6 +386,7 @@ public: // media management virtual void exitConference() = 0; virtual std::shared_ptr<Observable<std::shared_ptr<MediaFrame>>> getReceiveVideoFrameActiveWriter() = 0; + virtual void createSinks(const ConfInfo& infos) = 0; std::vector<std::map<std::string, std::string>> getConferenceInfos() const { diff --git a/src/conference.cpp b/src/conference.cpp index 9959050f9524997dcf90cf113069c4f7c0701adc..3556979e2d0134004fdfec0e04b327fe3679a680 100644 --- a/src/conference.cpp +++ b/src/conference.cpp @@ -71,6 +71,7 @@ Conference::Conference() // Handle participants showing their video std::unique_lock<std::mutex> lk(shared->videoToCallMtx_); for (const auto& info : infos) { + auto deviceId = ""; std::string uri = ""; auto it = shared->videoToCall_.find(info.source); if (it == shared->videoToCall_.end()) @@ -99,8 +100,10 @@ Conference::Conference() isLocalMuted = shared->isMediaSourceMuted(MediaType::MEDIA_AUDIO); } auto isModeratorMuted = shared->isMuted(peerID); + auto sinkId = shared->getConfID() + peerID + deviceId; newInfo.emplace_back(ParticipantInfo {std::move(uri), - "", + std::move(deviceId), + std::move(sinkId), active, info.x, info.y, @@ -118,17 +121,29 @@ Conference::Conference() lk.unlock(); // Handle participants not present in the video mixer for (const auto& subCall : subCalls) { + auto deviceId = ""; std::string uri = ""; if (auto call = getCall(subCall)) uri = call->getPeerNumber(); auto isModerator = shared->isModerator(uri); - newInfo.emplace_back(ParticipantInfo { - std::move(uri), "", false, 0, 0, 0, 0, true, false, false, isModerator}); + auto sinkId = shared->getConfID() + string_remove_suffix(uri, '@') + deviceId; + newInfo.emplace_back(ParticipantInfo {std::move(uri), + std::move(deviceId), + std::move(sinkId), + false, + 0, + 0, + 0, + 0, + true, + false, + false, + isModerator}); } // Add host in confInfo with audio and video muted if detached if (shared->getState() == State::ACTIVE_DETACHED) newInfo.emplace_back( - ParticipantInfo {"", "", false, 0, 0, 0, 0, true, true, false, true}); + ParticipantInfo {"", "", "", false, 0, 0, 0, 0, true, true, false, true}); shared->updateConferenceInfo(std::move(newInfo)); }); @@ -166,6 +181,12 @@ Conference::~Conference() call->peerRecording(true); } } + for (auto it = confSinksMap_.begin(); it != confSinksMap_.end();) { + if (videoMixer_) + videoMixer_->detach(it->second.get()); + it->second->stop(); + it = confSinksMap_.erase(it); + } #endif // ENABLE_VIDEO #ifdef ENABLE_PLUGIN { @@ -511,10 +532,29 @@ Conference::sendConferenceInfos() } } + + auto confInfo = getConfInfoHostUri("", ""); + createSinks(confInfo); + // Inform client that layout has changed jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>(id_, - getConfInfoHostUri("", "") - .toVectorMapStringString()); + confInfo.toVectorMapStringString()); +} + +void +Conference::createSinks(const ConfInfo& infos) +{ +#ifdef ENABLE_VIDEO + std::lock_guard<std::mutex> lk(sinksMtx_); + if (!videoMixer_) + return; + + Manager::instance().createSinkClients(getConfID(), + infos, + std::static_pointer_cast<video::VideoGenerator>( + videoMixer_), + confSinksMap_); +#endif } void diff --git a/src/conference.h b/src/conference.h index 8fcb915d7e471dd5dd103628cf7ecb383663b42b..ce631b4df04a9504e3ea21ad28190924ffb97242 100644 --- a/src/conference.h +++ b/src/conference.h @@ -42,6 +42,10 @@ #include "plugin/streamdata.h" #endif +#ifdef ENABLE_VIDEO +#include <video/sinkclient.h> +#endif + namespace jami { class Call; @@ -56,6 +60,7 @@ struct ParticipantInfo { std::string uri; std::string device; + std::string sinkId; bool active {false}; int x {0}; int y {0}; @@ -70,6 +75,7 @@ struct ParticipantInfo { uri = v["uri"].asString(); device = v["device"].asString(); + sinkId = v["sinkId"].asString(); active = v["active"].asBool(); x = v["x"].asInt(); y = v["y"].asInt(); @@ -86,6 +92,7 @@ struct ParticipantInfo Json::Value val; val["uri"] = uri; val["device"] = device; + val["sinkId"] = sinkId; val["active"] = active; val["x"] = x; val["y"] = y; @@ -102,6 +109,7 @@ struct ParticipantInfo { return {{"uri", uri}, {"device", device}, + {"sinkId", sinkId}, {"active", active ? "true" : "false"}, {"x", std::to_string(x)}, {"y", std::to_string(y)}, @@ -115,9 +123,10 @@ struct ParticipantInfo friend bool operator==(const ParticipantInfo& p1, const ParticipantInfo& p2) { - return p1.uri == p2.uri and p1.device == p2.device and p1.active == p2.active - and p1.x == p2.x and p1.y == p2.y and p1.w == p2.w and p1.h == p2.h - and p1.videoMuted == p2.videoMuted and p1.audioLocalMuted == p2.audioLocalMuted + return p1.uri == p2.uri and p1.device == p2.device and p1.sinkId == p2.sinkId + and p1.active == p2.active and p1.x == p2.x and p1.y == p2.y and p1.w == p2.w + and p1.h == p2.h and p1.videoMuted == p2.videoMuted + and p1.audioLocalMuted == p2.audioLocalMuted and p1.audioModeratorMuted == p2.audioModeratorMuted and p1.isModerator == p2.isModerator; } @@ -312,7 +321,7 @@ public: } void updateConferenceInfo(ConfInfo confInfo); - + void createSinks(const ConfInfo& infos); void setModerator(const std::string& uri, const bool& state); void muteParticipant(const std::string& uri, const bool& state); void hangupParticipant(const std::string& participant_id); @@ -348,6 +357,7 @@ private: std::string mediaInput_ {}; std::string mediaSecondaryInput_ {}; std::shared_ptr<video::VideoMixer> videoMixer_; + std::map<std::string, std::shared_ptr<video::SinkClient>> confSinksMap_ {}; #endif std::shared_ptr<jami::AudioInput> audioMixer_; @@ -384,6 +394,8 @@ private: std::string_view findHostforRemoteParticipant(std::string_view uri); std::shared_ptr<Call> getCallFromPeerID(std::string_view peerID); + std::mutex sinksMtx_ {}; + #ifdef ENABLE_PLUGIN /** * Call Streams and some typedefs diff --git a/src/manager.cpp b/src/manager.cpp index c5fc12dbc34ce51f2a166077afa2e5a615582cf2..4ff465a891d75dda7e013d0c86194d65b5c74cd9 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -86,6 +86,7 @@ using random_device = dht::crypto::random_device; #include "libav_utils.h" #include "video/sinkclient.h" +#include "video/video_base.h" #include "media/video/video_mixer.h" #include "audio/tonecontrol.h" @@ -3395,6 +3396,59 @@ Manager::createSinkClient(const std::string& id, bool mixer) return sink; } +void +Manager::createSinkClients(const std::string& callId, + const ConfInfo& infos, + const std::shared_ptr<video::VideoGenerator>& videoStream, + std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap) +{ + std::set<std::string> sinkIdsList {}; + + // create video sinks + for (const auto& participant : infos) { + std::string sinkId = participant.sinkId; + if (sinkId.empty()) { + sinkId = callId; + sinkId += string_remove_suffix(participant.uri, '@') + participant.device; + } + if (participant.w && participant.h) { + auto currentSink = getSinkClient(sinkId); + if (currentSink) { + videoStream->detach(currentSink.get()); + currentSink->stop(); + currentSink->start(); + currentSink->setFramePosition(participant.x, participant.y); + currentSink->setFrameSize(participant.w, participant.h); + videoStream->attach(currentSink.get()); + sinkIdsList.emplace(sinkId); + continue; + } + auto newSink = createSinkClient(sinkId); + newSink->start(); + newSink->setFramePosition(participant.x, participant.y); + newSink->setFrameSize(participant.w, participant.h); + + videoStream->attach(newSink.get()); + + sinksMap.emplace(sinkId, newSink); + sinkIdsList.emplace(sinkId); + } else { + sinkIdsList.erase(sinkId); + } + } + + // remove any non used video sink + for (auto it = sinksMap.begin(); it != sinksMap.end();) { + if (sinkIdsList.find(it->first) == sinkIdsList.end()) { + videoStream->detach(it->second.get()); + it->second->stop(); + it = sinksMap.erase(it); + } else { + it++; + } + } +} + std::shared_ptr<video::SinkClient> Manager::getSinkClient(const std::string& id) { diff --git a/src/manager.h b/src/manager.h index 3fb56f22b0fa138037bc300a33b3c002472602ff..b7b9b14e50ee4d0be25d674f421a32754c4d56b4 100644 --- a/src/manager.h +++ b/src/manager.h @@ -54,6 +54,7 @@ class io_context; namespace jami { namespace video { class SinkClient; +class VideoGenerator; } class ChannelSocket; class RingBufferPool; @@ -980,6 +981,19 @@ public: std::shared_ptr<video::SinkClient> createSinkClient(const std::string& id = "", bool mixer = false); + /** + * Create a SinkClient instance for each participant in a conference, store it in an internal + * cache as a weak_ptr and populates sinksMap with sink ids and shared_ptrs. + * @param callId + * @param infos ConferenceInfos that will create the sinks + * @param videoStream the the VideoGenerator to with the sinks should be attached + * @param sinksMap A map between sink ids and the respective shared pointer. + */ + void createSinkClients(const std::string& callId, + const ConfInfo& infos, + const std::shared_ptr<video::VideoGenerator>& videoStream, + std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap); + /** * Return an existing SinkClient instance as a shared_ptr associated to the given identifier. * Return an empty shared_ptr (nullptr) if nothing found. diff --git a/src/media/video/sinkclient.cpp b/src/media/video/sinkclient.cpp index 5249d94f9bb5a7b09162658736e5c6de39fd573b..75ed7bc925238b0e7cea187b1b349c9968a1695e 100644 --- a/src/media/video/sinkclient.cpp +++ b/src/media/video/sinkclient.cpp @@ -286,6 +286,7 @@ bool SinkClient::stop() noexcept { setFrameSize(0, 0); + setFramePosition(0, 0); shm_.reset(); return true; } @@ -308,6 +309,7 @@ bool SinkClient::stop() noexcept { setFrameSize(0, 0); + setFramePosition(0, 0); return true; } @@ -343,6 +345,16 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, if (avTarget_.push) { auto outFrame = std::make_unique<VideoFrame>(); outFrame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p)); + if (height_ != outFrame->pointer()->height) { + outFrame->pointer()->crop_top = y_; + outFrame->pointer()->crop_bottom = outFrame->pointer()->height - y_ - height_; + } + if (width_ != outFrame->pointer()->width) { + outFrame->pointer()->crop_left = x_; + outFrame->pointer()->crop_right = outFrame->pointer()->width - x_ - width_; + } + av_frame_apply_cropping(outFrame->pointer(), AV_FRAME_CROP_UNALIGNED); + avTarget_.push(std::move(outFrame)); } @@ -352,7 +364,7 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, #endif if (doTransfer) { - std::shared_ptr<VideoFrame> frame; + std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>(); #ifdef RING_ACCEL auto desc = av_pix_fmt_desc_get( (AVPixelFormat)(std::static_pointer_cast<VideoFrame>(frame_p))->format()); @@ -365,17 +377,28 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, JAMI_ERR("Accel failure: %s", e.what()); return; } - } - else + } else #endif - frame = std::static_pointer_cast<VideoFrame>(frame_p); + frame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p)); + + if (height_ != frame->pointer()->height) { + frame->pointer()->crop_top = y_; + frame->pointer()->crop_bottom = frame->pointer()->height - y_ - height_; + } + if (width_ != frame->pointer()->width) { + frame->pointer()->crop_left = x_; + frame->pointer()->crop_right = frame->pointer()->width - x_ - width_; + } + + av_frame_apply_cropping(frame->pointer(), AV_FRAME_CROP_UNALIGNED); + int angle = frame->getOrientation(); if (angle != rotation_) { filter_ = getTransposeFilter(angle, FILTER_INPUT_NAME, frame->width(), frame->height(), - AV_PIX_FMT_RGB32, + frame->format(), false); rotation_ = angle; } @@ -384,6 +407,7 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, frame = std::static_pointer_cast<VideoFrame>( std::shared_ptr<MediaFrame>(filter_->readOutput())); } + if (frame->height() != height_ || frame->width() != width_) { setFrameSize(0, 0); setFrameSize(frame->width(), frame->height()); @@ -436,5 +460,12 @@ SinkClient::setFrameSize(int width, int height) } } +void +SinkClient::setFramePosition(int x, int y) +{ + x_ = x; + y_ = y; +} + } // namespace video } // namespace jami diff --git a/src/media/video/sinkclient.h b/src/media/video/sinkclient.h index 85571ca3e85131ea4c6637f3e4fb793b0dc39813..fab57d0ee2a4a03a2de24f7305da30d7143b224a 100644 --- a/src/media/video/sinkclient.h +++ b/src/media/video/sinkclient.h @@ -74,6 +74,7 @@ public: bool stop() noexcept; void setFrameSize(int width, int height); + void setFramePosition(int x, int y); void registerTarget(const DRing::SinkTarget& target) noexcept { target_ = target; } void registerAVTarget(const DRing::AVSinkTarget& target) noexcept { avTarget_ = target; } @@ -83,6 +84,8 @@ private: const bool mixer_; int width_ {0}; int height_ {0}; + int x_ {0}; + int y_ {0}; bool started_ {false}; // used to arbitrate client's stop signal. int rotation_ {0}; DRing::SinkTarget target_; diff --git a/src/media/video/video_receive_thread.cpp b/src/media/video/video_receive_thread.cpp index 690377af133f7e5c01603eb47f7391f1a81d02cf..3a0deda02cfbfe8e6eb80b9d214b7c78e4021df1 100644 --- a/src/media/video/video_receive_thread.cpp +++ b/src/media/video/video_receive_thread.cpp @@ -85,6 +85,7 @@ VideoReceiveThread::setup() videoDecoder_->setResolutionChangedCallback([this] (int width, int height){ dstWidth_ = width; dstHeight_ = height; + sink_->setFrameSize(dstWidth_, dstHeight_); }); dstWidth_ = args_.width; diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp index ca23674eeedea43df085ed70db6a95b739008b66..a4b12b1fa222af191c642312a1f47060f9ffdd3e 100644 --- a/src/sip/sipcall.cpp +++ b/src/sip/sipcall.cpp @@ -53,6 +53,7 @@ #include "jami/videomanager_interface.h" #include <chrono> #include <libavutil/display.h> +#include <video/sinkclient.h> #endif #include "audio/ringbufferpool.h" #include "jamidht/channeled_transport.h" @@ -2040,6 +2041,17 @@ SIPCall::stopAllMedia() audioRtp->stop(); #ifdef ENABLE_VIDEO auto const& videoRtp = getVideoRtp(); + + for (auto it = callSinksMap_.begin(); it != callSinksMap_.end();) { + auto& videoReceive = videoRtp->getVideoReceive(); + if (videoReceive) { + videoReceive->detach(it->second.get()); + } + + it->second->stop(); + it = callSinksMap_.erase(it); + } + if (videoRtp) videoRtp->stop(); #endif @@ -2706,6 +2718,27 @@ SIPCall::addDummyVideoRtpSession() return {}; } +void +SIPCall::createSinks(const ConfInfo& infos) +{ +#ifdef ENABLE_VIDEO + if (!hasVideo()) + return; + + std::lock_guard<std::mutex> lk(sinksMtx_); + auto videoRtp = getVideoRtp(); + auto& videoReceive = videoRtp->getVideoReceive(); + if (!videoReceive) + return; + auto id = getConfId().empty() ? getCallId() : getConfId(); + Manager::instance().createSinkClients(id, + infos, + std::static_pointer_cast<video::VideoGenerator>( + videoReceive), + callSinksMap_); +#endif +} + std::shared_ptr<AudioRtpSession> SIPCall::getAudioRtp() const { diff --git a/src/sip/sipcall.h b/src/sip/sipcall.h index 80b60d91e3d45e3ff4e0fc1e781a1fe7deeebad6..2c44911e7539e4ca1b62082b62671ec56309de99 100644 --- a/src/sip/sipcall.h +++ b/src/sip/sipcall.h @@ -155,6 +155,9 @@ public: void exitConference() override; std::shared_ptr<Observable<std::shared_ptr<MediaFrame>>> getReceiveVideoFrameActiveWriter() override; + std::mutex sinksMtx_; + void createSinks(const ConfInfo& infos) override; + std::map<std::string, std::shared_ptr<video::SinkClient>> callSinksMap_ {}; bool hasVideo() const override; bool isCaptureDeviceMuted(const MediaType& mediaType) const override; bool isSrtpEnabled() const { return srtpEnabled_; } diff --git a/test/unitTest/call/conference.cpp b/test/unitTest/call/conference.cpp index 725a8a82f1eef15d19abc21f3db43ac243a21cc7..efaad83c69e26efa74b09aa7112d86c8aab7c08a 100644 --- a/test/unitTest/call/conference.cpp +++ b/test/unitTest/call/conference.cpp @@ -31,6 +31,7 @@ #include "account_const.h" #include "common.h" #include "media_const.h" +#include "video/sinkclient.h" using namespace DRing::Account; @@ -70,11 +71,13 @@ private: void testGetConference(); void testModeratorMuteUpdateParticipantsInfos(); void testAudioVideoMutedStates(); + void testCreateParticipantsSinks(); CPPUNIT_TEST_SUITE(ConferenceTest); CPPUNIT_TEST(testGetConference); CPPUNIT_TEST(testModeratorMuteUpdateParticipantsInfos); CPPUNIT_TEST(testAudioVideoMutedStates); + CPPUNIT_TEST(testCreateParticipantsSinks); CPPUNIT_TEST_SUITE_END(); // Common parts @@ -230,15 +233,18 @@ ConferenceTest::testGetConference() hangupConference(); CPPUNIT_ASSERT(Manager::instance().getConferenceList().size() == 0); + + DRing::unregisterSignalHandlers(); } void ConferenceTest::testModeratorMuteUpdateParticipantsInfos() { + registerSignalHandlers(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); auto bobUri = bobAccount->getUsername(); - registerSignalHandlers(); startConference(); JAMI_INFO("Play with mute from the moderator"); @@ -251,6 +257,8 @@ ConferenceTest::testModeratorMuteUpdateParticipantsInfos() cv.wait_for(lk, std::chrono::seconds(5), [&] { return !bobCall.moderatorMuted.load(); })); hangupConference(); + + DRing::unregisterSignalHandlers(); } void @@ -299,8 +307,41 @@ ConferenceTest::testAudioVideoMutedStates() })); hangupConference(); + + DRing::unregisterSignalHandlers(); } +void +ConferenceTest::testCreateParticipantsSinks() +{ + registerSignalHandlers(); + + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto bobUri = bobAccount->getUsername(); + auto carlaUri = carlaAccount->getUsername(); + + startConference(); + + auto infos = Manager::instance().getConferenceInfos(confId); + + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(5), [&] { + bool sinksStatus = true; + for (auto& info : infos) { + if (info["uri"] == bobUri) { + sinksStatus &= (Manager::instance().getSinkClient(info["sinkId"]) != nullptr); + } else if (info["uri"] == carlaUri) { + sinksStatus &= (Manager::instance().getSinkClient(info["sinkId"]) != nullptr); + } + } + return sinksStatus; + })); + + hangupConference(); + + DRing::unregisterSignalHandlers(); +} } // namespace test } // namespace jami