Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #pragma once
18 :
19 : #ifdef HAVE_CONFIG_H
20 : #include "config.h"
21 : #endif
22 :
23 : #include <chrono>
24 : #include <set>
25 : #include <string>
26 : #include <memory>
27 : #include <vector>
28 : #include <string_view>
29 : #include <map>
30 : #include <functional>
31 :
32 : #include "conference_protocol.h"
33 : #include "media/audio/audio_input.h"
34 : #include "media/media_attribute.h"
35 : #include "media/recordable.h"
36 :
37 : #ifdef ENABLE_PLUGIN
38 : #include "plugin/streamdata.h"
39 : #endif
40 :
41 : #ifdef ENABLE_VIDEO
42 : #include "media/video/sinkclient.h"
43 : #endif
44 :
45 : #include <json/json.h>
46 :
47 : namespace jami {
48 :
49 : class Call;
50 : class Account;
51 :
52 : #ifdef ENABLE_VIDEO
53 : namespace video {
54 : class VideoMixer;
55 : }
56 : #endif
57 :
58 : // info for a stream
59 : struct ParticipantInfo
60 : {
61 : std::string uri;
62 : std::string device;
63 : std::string sinkId; // stream ID
64 : bool active {false};
65 : int x {0};
66 : int y {0};
67 : int w {0};
68 : int h {0};
69 : bool videoMuted {false};
70 : bool audioLocalMuted {false};
71 : bool audioModeratorMuted {false};
72 : bool isModerator {false};
73 : bool handRaised {false};
74 : bool voiceActivity {false};
75 : bool recording {false};
76 :
77 565 : void fromJson(const Json::Value& v)
78 : {
79 565 : uri = v["uri"].asString();
80 565 : device = v["device"].asString();
81 565 : sinkId = v["sinkId"].asString();
82 565 : active = v["active"].asBool();
83 565 : x = v["x"].asInt();
84 565 : y = v["y"].asInt();
85 565 : w = v["w"].asInt();
86 565 : h = v["h"].asInt();
87 565 : videoMuted = v["videoMuted"].asBool();
88 565 : audioLocalMuted = v["audioLocalMuted"].asBool();
89 565 : audioModeratorMuted = v["audioModeratorMuted"].asBool();
90 565 : isModerator = v["isModerator"].asBool();
91 565 : handRaised = v["handRaised"].asBool();
92 565 : voiceActivity = v["voiceActivity"].asBool();
93 565 : recording = v["recording"].asBool();
94 565 : }
95 :
96 575 : Json::Value toJson() const
97 : {
98 575 : Json::Value val;
99 577 : val["uri"] = uri;
100 577 : val["device"] = device;
101 578 : val["sinkId"] = sinkId;
102 575 : val["active"] = active;
103 577 : val["x"] = x;
104 578 : val["y"] = y;
105 577 : val["w"] = w;
106 575 : val["h"] = h;
107 576 : val["videoMuted"] = videoMuted;
108 574 : val["audioLocalMuted"] = audioLocalMuted;
109 576 : val["audioModeratorMuted"] = audioModeratorMuted;
110 577 : val["isModerator"] = isModerator;
111 578 : val["handRaised"] = handRaised;
112 576 : val["voiceActivity"] = voiceActivity;
113 576 : val["recording"] = recording;
114 578 : return val;
115 0 : }
116 :
117 831 : std::map<std::string, std::string> toMap() const
118 : {
119 831 : return {{"uri", uri},
120 831 : {"device", device},
121 831 : {"sinkId", sinkId},
122 831 : {"active", active ? "true" : "false"},
123 1662 : {"x", std::to_string(x)},
124 1662 : {"y", std::to_string(y)},
125 1662 : {"w", std::to_string(w)},
126 1662 : {"h", std::to_string(h)},
127 831 : {"videoMuted", videoMuted ? "true" : "false"},
128 831 : {"audioLocalMuted", audioLocalMuted ? "true" : "false"},
129 831 : {"audioModeratorMuted", audioModeratorMuted ? "true" : "false"},
130 831 : {"isModerator", isModerator ? "true" : "false"},
131 831 : {"handRaised", handRaised ? "true" : "false"},
132 831 : {"voiceActivity", voiceActivity ? "true" : "false"},
133 23268 : {"recording", recording ? "true" : "false"}};
134 : }
135 :
136 0 : friend bool operator==(const ParticipantInfo& p1, const ParticipantInfo& p2)
137 : {
138 0 : return p1.uri == p2.uri and p1.device == p2.device and p1.sinkId == p2.sinkId
139 0 : and p1.active == p2.active and p1.x == p2.x and p1.y == p2.y and p1.w == p2.w
140 0 : and p1.h == p2.h and p1.videoMuted == p2.videoMuted
141 0 : and p1.audioLocalMuted == p2.audioLocalMuted
142 0 : and p1.audioModeratorMuted == p2.audioModeratorMuted
143 0 : and p1.isModerator == p2.isModerator and p1.handRaised == p2.handRaised
144 0 : and p1.voiceActivity == p2.voiceActivity and p1.recording == p2.recording;
145 : }
146 :
147 : friend bool operator!=(const ParticipantInfo& p1, const ParticipantInfo& p2)
148 : {
149 : return !(p1 == p2);
150 : }
151 : };
152 :
153 : struct ConfInfo : public std::vector<ParticipantInfo>
154 : {
155 : int h {0};
156 : int w {0};
157 : int v {1}; // Supported conference protocol version
158 : int layout {0};
159 :
160 0 : friend bool operator==(const ConfInfo& c1, const ConfInfo& c2)
161 : {
162 0 : if (c1.h != c2.h or c1.w != c2.w)
163 0 : return false;
164 0 : if (c1.size() != c2.size())
165 0 : return false;
166 :
167 0 : for (auto& p1 : c1) {
168 0 : auto it = std::find_if(c2.begin(), c2.end(), [&p1](const ParticipantInfo& p2) {
169 0 : return p1 == p2;
170 : });
171 0 : if (it != c2.end())
172 0 : continue;
173 : else
174 0 : return false;
175 : }
176 0 : return true;
177 : }
178 :
179 0 : friend bool operator!=(const ConfInfo& c1, const ConfInfo& c2) { return !(c1 == c2); }
180 :
181 : std::vector<std::map<std::string, std::string>> toVectorMapStringString() const;
182 : std::string toString() const;
183 : };
184 :
185 : using CallIdSet = std::set<std::string>;
186 : using clock = std::chrono::steady_clock;
187 :
188 : class Conference : public Recordable, public std::enable_shared_from_this<Conference>
189 : {
190 : public:
191 : enum class State { ACTIVE_ATTACHED, ACTIVE_DETACHED, HOLD };
192 :
193 : /**
194 : * Constructor for this class, increment static counter
195 : */
196 : explicit Conference(const std::shared_ptr<Account>&,
197 : const std::string& confId = "");
198 :
199 : /**
200 : * Destructor for this class, decrement static counter
201 : */
202 : ~Conference();
203 :
204 : /**
205 : * Return the conference id
206 : */
207 1815 : const std::string& getConfId() const { return id_; }
208 :
209 255 : std::shared_ptr<Account> getAccount() const { return account_.lock(); }
210 :
211 : std::string getAccountId() const;
212 :
213 : /**
214 : * Return the current conference state
215 : */
216 : State getState() const;
217 :
218 : /**
219 : * Set conference state
220 : */
221 : void setState(State state);
222 :
223 : /**
224 : * Set a callback that will be called when the conference will be destroyed
225 : */
226 14 : void onShutdown(std::function<void(int)> cb) { shutdownCb_ = std::move(cb); }
227 :
228 : /**
229 : * Return a string description of the conference state
230 : */
231 291 : static constexpr const char* getStateStr(State state)
232 : {
233 291 : switch (state) {
234 176 : case State::ACTIVE_ATTACHED:
235 176 : return "ACTIVE_ATTACHED";
236 115 : case State::ACTIVE_DETACHED:
237 115 : return "ACTIVE_DETACHED";
238 0 : case State::HOLD:
239 0 : return "HOLD";
240 0 : default:
241 0 : return "";
242 : }
243 : }
244 :
245 207 : const char* getStateStr() const { return getStateStr(confState_); }
246 :
247 : /**
248 : * Set the mute state of the local host
249 : */
250 : void setLocalHostMuteState(MediaType type, bool muted);
251 :
252 : /**
253 : * Get the mute state of the local host
254 : */
255 : bool isMediaSourceMuted(MediaType type) const;
256 :
257 : /**
258 : * Process a media change request.
259 : * Used to change the media attributes of the host.
260 : *
261 : * @param remoteMediaList new media list from the remote
262 : * @return true on success
263 : */
264 : bool requestMediaChange(const std::vector<libjami::MediaMap>& mediaList);
265 :
266 : /**
267 : * Process incoming media change request.
268 : *
269 : * @param callId the call ID
270 : * @param remoteMediaList new media list from the remote
271 : */
272 : void handleMediaChangeRequest(const std::shared_ptr<Call>& call,
273 : const std::vector<libjami::MediaMap>& remoteMediaList);
274 :
275 : /**
276 : * Add a new subcall to the conference
277 : */
278 : void addSubCall(const std::string& callId);
279 :
280 : /**
281 : * Remove a subcall from the conference
282 : */
283 : void removeSubCall(const std::string& callId);
284 :
285 : /**
286 : * Attach host
287 : */
288 : void attachHost(const std::vector<libjami::MediaMap>& mediaList = {});
289 :
290 : /**
291 : * Detach local audio/video from the conference
292 : */
293 : void detachHost();
294 :
295 : /**
296 : * Get the participant list for this conference
297 : */
298 : CallIdSet getSubCalls() const;
299 :
300 : /**
301 : * Start/stop recording toggle
302 : */
303 : bool toggleRecording() override;
304 :
305 : void switchInput(const std::string& input);
306 : void setActiveParticipant(const std::string& participant_id);
307 : void setActiveStream(const std::string& streamId, bool state);
308 : void setLayout(int layout);
309 :
310 : void onConfOrder(const std::string& callId, const std::string& order);
311 :
312 : bool isVideoEnabled() const;
313 :
314 : #ifdef ENABLE_VIDEO
315 : void createSinks(const ConfInfo& infos);
316 : std::shared_ptr<video::VideoMixer> getVideoMixer();
317 : std::string getVideoInput() const;
318 : #endif
319 :
320 0 : std::vector<std::map<std::string, std::string>> getConferenceInfos() const
321 : {
322 0 : std::lock_guard lk(confInfoMutex_);
323 0 : return confInfo_.toVectorMapStringString();
324 0 : }
325 :
326 : void updateConferenceInfo(ConfInfo confInfo);
327 : void setModerator(const std::string& uri, const bool& state);
328 : void hangupParticipant(const std::string& accountUri, const std::string& deviceId = "");
329 : void setHandRaised(const std::string& uri, const bool& state);
330 : void setVoiceActivity(const std::string& streamId, const bool& newState);
331 :
332 : void muteParticipant(const std::string& uri, const bool& state);
333 : void muteLocalHost(bool is_muted, const std::string& mediaType);
334 : bool isRemoteParticipant(const std::string& uri);
335 : void mergeConfInfo(ConfInfo& newInfo, const std::string& peerURI);
336 :
337 : /**
338 : * The client shows one tile per stream (video/audio related to a media)
339 : * @note for now, in conferences we can only mute the audio of a call
340 : * @todo add a track (audio OR video) parameter to know what we want to mute
341 : * @param accountUri Account of the stream
342 : * @param deviceId Device of the stream
343 : * @param streamId Stream to mute
344 : * @param state True to mute, false to unmute
345 : */
346 : void muteStream(const std::string& accountUri,
347 : const std::string& deviceId,
348 : const std::string& streamId,
349 : const bool& state);
350 : void updateMuted();
351 : void updateRecording();
352 :
353 : void updateVoiceActivity();
354 :
355 : std::shared_ptr<Call> getCallFromPeerID(std::string_view peerId);
356 :
357 : /**
358 : * Announce to the client that medias are successfully negotiated
359 : */
360 : void reportMediaNegotiationStatus();
361 :
362 : /**
363 : * Retrieve current medias list
364 : * @return current medias
365 : */
366 : std::vector<libjami::MediaMap> currentMediaList() const;
367 :
368 : // Update layout if recording changes
369 : void stopRecording() override;
370 : bool startRecording(const std::string& path) override;
371 :
372 : /**
373 : * @return Conference duration in milliseconds
374 : */
375 14 : std::chrono::milliseconds getDuration() const
376 : {
377 14 : return duration_start_ == clock::time_point::min()
378 0 : ? std::chrono::milliseconds::zero()
379 14 : : std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()
380 28 : - duration_start_);
381 : }
382 :
383 : private:
384 78 : std::weak_ptr<Conference> weak()
385 : {
386 78 : return std::static_pointer_cast<Conference>(shared_from_this());
387 : }
388 :
389 : static std::shared_ptr<Call> getCall(const std::string& callId);
390 : bool isModerator(std::string_view uri) const;
391 : bool isHandRaised(std::string_view uri) const;
392 : bool isVoiceActive(std::string_view uri) const;
393 : void updateModerators();
394 : void updateHandsRaised();
395 : void muteHost(bool state);
396 : void muteCall(const std::string& callId, bool state);
397 :
398 : void foreachCall(const std::function<void(const std::shared_ptr<Call>& call)>& cb);
399 :
400 : std::string id_;
401 : std::weak_ptr<Account> account_;
402 : State confState_ {State::ACTIVE_DETACHED};
403 : mutable std::mutex subcallsMtx_ {};
404 : CallIdSet subCalls_;
405 : std::string mediaPlayerId_ {};
406 :
407 : mutable std::mutex confInfoMutex_ {};
408 : ConfInfo confInfo_ {};
409 :
410 : void sendConferenceInfos();
411 : std::shared_ptr<RingBuffer> ghostRingBuffer_;
412 :
413 : #ifdef ENABLE_VIDEO
414 : bool videoEnabled_;
415 : std::shared_ptr<video::VideoMixer> videoMixer_;
416 : std::map<std::string, std::shared_ptr<video::SinkClient>> confSinksMap_ {};
417 : #endif
418 :
419 : std::shared_ptr<jami::AudioInput> audioMixer_;
420 : std::set<std::string, std::less<>> moderators_ {};
421 : std::set<std::string, std::less<>> participantsMuted_ {};
422 : std::set<std::string, std::less<>> handsRaised_;
423 :
424 : // stream IDs
425 : std::set<std::string, std::less<>> streamsVoiceActive {};
426 :
427 : void initRecorder(std::shared_ptr<MediaRecorder>& rec);
428 : void deinitRecorder(std::shared_ptr<MediaRecorder>& rec);
429 :
430 : bool isMuted(std::string_view uri) const;
431 :
432 : ConfInfo getConfInfoHostUri(std::string_view localHostURI, std::string_view destURI);
433 : bool isHost(std::string_view uri) const;
434 : bool isHostDevice(std::string_view deviceId) const;
435 :
436 : /**
437 : * If the local host is participating in the conference (attached
438 : * mode ), this variable will hold the media source states
439 : * of the local host.
440 : */
441 : std::vector<MediaAttribute> hostSources_;
442 : // Because host doesn't have a call, we need to store the audio inputs
443 : std::map<std::string, std::shared_ptr<jami::AudioInput>> hostAudioInputs_;
444 :
445 : bool localModAdded_ {false};
446 :
447 : std::map<std::string, ConfInfo> remoteHosts_;
448 : #ifdef ENABLE_VIDEO
449 : void resizeRemoteParticipants(ConfInfo& confInfo, std::string_view peerURI);
450 : #endif
451 : std::string_view findHostforRemoteParticipant(std::string_view uri,
452 : std::string_view deviceId = "");
453 :
454 : std::shared_ptr<Call> getCallWith(const std::string& accountUri, const std::string& deviceId);
455 :
456 : std::mutex sinksMtx_ {};
457 :
458 : #ifdef ENABLE_PLUGIN
459 : /**
460 : * Call Streams and some typedefs
461 : */
462 : using AVMediaStream = Observable<std::shared_ptr<MediaFrame>>;
463 : using MediaStreamSubject = PublishMapSubject<std::shared_ptr<MediaFrame>, AVFrame*>;
464 :
465 : #ifdef ENABLE_VIDEO
466 : /**
467 : * Map: maps the VideoFrame to an AVFrame
468 : **/
469 : std::function<AVFrame*(const std::shared_ptr<jami::MediaFrame>&)> pluginVideoMap_ =
470 3667 : [](const std::shared_ptr<jami::MediaFrame>& m) -> AVFrame* {
471 3667 : return std::static_pointer_cast<VideoFrame>(m)->pointer();
472 : };
473 : #endif // ENABLE_VIDEO
474 :
475 : /**
476 : * @brief createConfAVStream
477 : * Creates a conf AV stream like video input, video receive, audio input or audio receive
478 : * @param StreamData
479 : * @param streamSource
480 : * @param mediaStreamSubject
481 : */
482 : void createConfAVStream(const StreamData& StreamData,
483 : AVMediaStream& streamSource,
484 : const std::shared_ptr<MediaStreamSubject>& mediaStreamSubject,
485 : bool force = false);
486 : /**
487 : * @brief createConfAVStreams
488 : * Creates all Conf AV Streams (2 if audio, 4 if audio video)
489 : */
490 : void createConfAVStreams();
491 :
492 : std::mutex avStreamsMtx_ {};
493 : std::map<std::string, std::shared_ptr<MediaStreamSubject>> confAVStreams;
494 : #endif // ENABLE_PLUGIN
495 :
496 : ConfProtocolParser parser_;
497 : std::string getRemoteId(const std::shared_ptr<jami::Call>& call) const;
498 :
499 : std::function<void(int)> shutdownCb_;
500 : clock::time_point duration_start_;
501 :
502 : /**
503 : * Initialize sources for host (takes default camera/audio)
504 : */
505 : void initSourcesForHost();
506 :
507 : /**
508 : * Take over media control from the call.
509 : * When a call joins a conference, the media control (mainly mute/un-mute
510 : * state of the local media source) will be handled by the conference and
511 : * the mixer.
512 : */
513 : void takeOverMediaSourceControl(const std::string& callId);
514 :
515 : /**
516 : * Bind host's audio
517 : */
518 : void bindHostAudio();
519 :
520 : /**
521 : * Unbind host's audio
522 : */
523 : void unbindHostAudio();
524 :
525 : /**
526 : * Bind call's audio to the conference
527 : */
528 : void bindSubCallAudio(const std::string& callId);
529 :
530 : /**
531 : * Unbind call's audio from the conference
532 : */
533 : void unbindSubCallAudio(const std::string& callId);
534 : };
535 :
536 : } // namespace jami
|