diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f987abf..0e3be88 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,9 +38,10 @@ jobs: sed -i -e 's|webrtc_path = ".*"|webrtc_path = "${{ github.workspace }}/webrtc-checkout/"|g' WORKSPACE # Don't `build ...`, as includes irrelevant targets in `webrtc-checkout`. bazel-7.4.1 build native/... + bazel-7.4.1 build native_with_state/... bazel-7.4.1 build web/... - name: 🔍 Test run: | bazel-7.4.1 test native/... - + # bazel-7.4.1 test native_with_state/... (no tests yet) diff --git a/WORKSPACE b/WORKSPACE index 13df3b4..7f410b9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -147,24 +147,3 @@ local_repository( name = "boringssl", path = webrtc_path + "webrtc/third_party/boringssl/src", ) - -# === Python === -http_archive( - name = "rules_python", - sha256 = "be04b635c7be4604be1ef20542e9870af3c49778ce841ee2d92fcb42f9d9516a", - strip_prefix = "rules_python-0.35.0", - url = "https://github.com/bazelbuild/rules_python/releases/download/0.35.0/rules_python-0.35.0.tar.gz", -) - -load("@rules_python//python:repositories.bzl", "py_repositories") -py_repositories() - -load("@rules_python//python:pip.bzl", "pip_parse") - -pip_parse( - name = "util_deps", - requirements_lock = "//web/utils:requirements.txt", -) - -load("@util_deps//:requirements.bzl", "install_deps") -install_deps() diff --git a/native_with_state/api/BUILD b/native_with_state/api/BUILD new file mode 100644 index 0000000..1f6ff00 --- /dev/null +++ b/native_with_state/api/BUILD @@ -0,0 +1,96 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:private"]) + +cc_library( + name = "media_api_client_interface", + hdrs = [ + "media_api_client_interface.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], + deps = [ + ":media_entries_resource", + ":media_stats_resource", + ":participants_resource", + ":session_control_resource", + ":video_assignment_resource", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/types:span", + "@webrtc", + ], +) + +cc_library( + name = "media_api_client_factory_interface", + hdrs = ["media_api_client_factory_interface.h"], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], + deps = [ + ":media_api_client_interface", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status:statusor", + "@webrtc", + ], +) + +cc_library( + name = "media_stats_resource", + hdrs = [ + "media_stats_resource.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], + deps = [ + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/types:optional", + ], +) + +cc_library( + name = "participants_resource", + hdrs = [ + "participants_resource.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], +) + +cc_library( + name = "video_assignment_resource", + hdrs = [ + "video_assignment_resource.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], + deps = ["@com_google_absl//absl/status"], +) + +cc_library( + name = "media_entries_resource", + hdrs = [ + "media_entries_resource.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], +) + +cc_library( + name = "session_control_resource", + hdrs = [ + "session_control_resource.h", + ], + visibility = ["@media_api_samples//native_with_state:__subpackages__"], + deps = ["@com_google_absl//absl/status"], +) diff --git a/native_with_state/api/media_api_client_factory_interface.h b/native_with_state/api/media_api_client_factory_interface.h new file mode 100644 index 0000000..29f9753 --- /dev/null +++ b/native_with_state/api/media_api_client_factory_interface.h @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_FACTORY_INTERFACE_H_ +#define NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_FACTORY_INTERFACE_H_ + +#include + +#include "absl/base/nullability.h" +#include "absl/status/statusor.h" +#include "native_with_state/api/media_api_client_interface.h" +#include "webrtc/api/scoped_refptr.h" + +namespace meet { + +// Interface for instantiating MediaApiClientInterfaces. +class MediaApiClientFactoryInterface { + public: + virtual ~MediaApiClientFactoryInterface() = default; + + virtual absl::StatusOr> + CreateMediaApiClient( + const MediaApiClientConfiguration& api_config, + absl::Nonnull> + api_session_observer) = 0; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_FACTORY_INTERFACE_H_ diff --git a/native_with_state/api/media_api_client_interface.h b/native_with_state/api/media_api_client_interface.h new file mode 100644 index 0000000..260ed02 --- /dev/null +++ b/native_with_state/api/media_api_client_interface.h @@ -0,0 +1,278 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_INTERFACE_H_ +#define NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_INTERFACE_H_ + +// This file contains the MediaApiClient interface. It is designed to +// utilize the PeerConnection interface as defined in +// https://w3c.github.io/webrtc-pc/#peer-to-peer-connections. +// +// It demonstrates "how-to", and establishes the required configurations and +// SCTP/SRTP connections with Meet servers. These connections enable the +// streaming of conference metadata, video, and audio streams from Google Meet +// conferences to the client. +// +// Note that this is a reference client. It is not intended to be a complete SDK +// with full support, customization, nor optimizations of WebRTC and real time +// communication. +// +// All conference media streams are "receive-only". Currently, Meet Media API +// does not support sending of media from the MediaApiClient interface into +// a conference. +// +// API requests from the client intended to affect application state of a +// conference or received media (e.g. change video resolution), are transmitted +// via SCTP data channels. This is in contrast to typical API requests over +// HTTP or RPC. +// +// The following steps are needed to setup a typical Media API session: +// +// 1. Create an implementation of the `MediaApiClientObserverInterface`. +// +// 2. Create a MediaApiClientInterface using an implementation of the +// `MediaApiClientFactoryInterface`. +// +// 3. Call `ConnectActiveConference` with the appropriate parameters. This +// initiates the connection with Meet servers. +// +// 4. Wait for the `OnJoined` callback to be invoked. +// +// 5. If video was enabled, send a `SetVideoAssignment` request via +// `SendRequest` method of the `MediaApiClientInterface`. No video will be +// transmitted from Meet servers to the client until a successful request has +// been sent. Check `video_assignment_resource.h` for more information. + +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "native_with_state/api/media_entries_resource.h" +#include "native_with_state/api/media_stats_resource.h" +#include "native_with_state/api/participants_resource.h" +#include "native_with_state/api/session_control_resource.h" +#include "native_with_state/api/video_assignment_resource.h" +#include "webrtc/api/ref_count.h" +#include "webrtc/api/scoped_refptr.h" +#include "webrtc/api/video/video_frame.h" + +namespace meet { + +struct MediaApiClientConfiguration { + // For values greater than zero, the Media API client will establish that many + // video SRTP streams. After the session is initialized, no other streams will + // be created nor intentionally terminated. All connections will be cleaned up + // after the session is complete. Up to three streams are supported and they + // are "receive-only". Attempts to set a value greater than three will result + // in an error. + uint32_t receiving_video_stream_count = 0; + // If audio is enabled, three "receive-only" audio SRTP streams will be + // created, always. After the session is initialized, no other streams will be + // created nor intentionally terminated. All connections will be cleaned up + // after the session is complete. + bool enable_audio_streams = false; +}; + +// Requests that can be sent to Meet servers. +// +// Requests can expect a corresponding response via the +// `MediaApiClientObserverInterface`. +using ResourceRequest = + std::variant; + +// Updates that can be received from Meet servers. +// +// Updates can be received in response to a request sent via `SendRequest` or +// from a push from Meet servers. +using ResourceUpdate = + std::variant; + +struct AudioFrame { + absl::Span pcm16; + int bits_per_sample; + int sample_rate; + size_t number_of_channels; + size_t number_of_frames; + // Contributing source (CSRC) of the current audio frame. This ID is used to + // identify which participant in the conference generated the frame. + // Integrators can cross reference this value with values pushed from Meet + // servers to the client via `MediaEntriesToClient` resource updates. + // + // https://www.w3.org/TR/webrtc/#dom-rtcrtpcontributingsource + uint32_t contributing_source; + // Synchronization source (SSRC) of the audio frame. This ID identifies which + // media stream the audio frame originated from. The ssrc is for debugging + // purposes only. + // + // https://www.w3.org/TR/webrtc/#dom-rtcrtpsynchronizationsource + uint32_t synchronization_source; +}; + +struct VideoFrame { + const webrtc::VideoFrame& frame; + // Contributing source (CSRC) of the current audio frame. This ID is used to + // identify which participant in the conference generated the frame. + // Integrators can cross reference this value with values pushed from Meet + // servers to the client via `MediaEntriesToClient` resource updates. + // + // https://www.w3.org/TR/webrtc/#dom-rtcrtpcontributingsource + uint32_t contributing_source; + // Synchronization source (SSRC) of the video frame. This ID identifies which + // media stream the video frame originated from. The ssrc is for debugging + // purposes only. + // + // https://www.w3.org/TR/webrtc/#dom-rtcrtpsynchronizationsource + uint32_t synchronization_source; +}; + +// Interface for observing client events. +// +// Methods are invoked on internal threads, and therefore observer +// implementations must offload non-trivial work to other threads. Otherwise, +// they risk blocking the client. +class MediaApiClientObserverInterface : public webrtc::RefCountInterface { + public: + ~MediaApiClientObserverInterface() override = default; + + // Invoked when the client has entered the Joined state. + // + // Once this is invoked, the client is fully operational and will remain in + // this state until `OnDisconnected` is invoked. + virtual void OnJoined() = 0; + + // Invoked when the client disconnects for whatever reason. + // + // This will only be called after `ConnectActiveConference` is called. + // + // This will be called once and only once, either before or after `OnJoined` + // is called. + // + // Once this is invoked, no other callbacks will be invoked. + // + // Disconnections are either graceful or ungraceful. Disconnections are + // considered graceful if the client receives a + // `SessionControlChannelToClient` resource update with a session status of + // SESSION_DISCONNECTED, or if `LeaveConference` is called while the client is + // joining the conference. All other disconnections are considered ungraceful + // (PeerConnection closed, Meet servers unreachable, etc). + // + // This client implementation passes an OK status for graceful disconnections + // and an error status for ungraceful disconnections. Graceful disconnections + // can be analyzed by checking the `SessionControlChannelToClient` resource + // update received via `OnResourceUpdate`. + virtual void OnDisconnected(absl::Status status) = 0; + + // The following methods will only be invoked while in the Joined state. + + // Invoked when a resource update is received from Meet servers. + // + // This can be in response to a request sent via `SendRequest` or a push from + // Meet servers. + virtual void OnResourceUpdate(ResourceUpdate update) = 0; + + // Callbacks for receiving media frames. + // + // Note that audio frames will not be received for muted participants, and + // video frames will not be received for participants that have their video + // disabled (i.e. their video is muted). + virtual void OnAudioFrame(AudioFrame frame) = 0; + virtual void OnVideoFrame(VideoFrame frame) = 0; +}; + +// Interface for the Media API client. +// +// This client implementation is meant to be used for one connection lifetime +// and then thrown away; if integrators need a new connection, they should +// create a new instance of MediaApiClientInterface. +class MediaApiClientInterface { + public: + virtual ~MediaApiClientInterface() = default; + + // Attempts to connect with Meet servers. This process involves + // communicating the intent to join an active Meet conference. It establishes + // the signaled SRTP and SCTP connections with the backend. + // + // If the client successfully joins the conference, the observer's `OnJoined` + // will be called. If this method returns OK and joining fails, the observer's + // `OnDisconnected` method will be called. If the client successfully joins, + // `OnDisconnected` will be invoked when the client leaves the conference for + // whatever reason. + // + // Once fully joined, if audio was enabled, the client will begin receiving + // any available streams immediately. If video was enabled, the client will + // not receive any video streams until a `SetVideoAssignment` request is + // successfully sent to Meet servers and applied.. + // + // The provided join_endpoint must be a valid URL including the protocol and + // host. There aren't very robust checks performed on the provided URL. It is + // expected that the provided URL is well formed. + virtual absl::Status ConnectActiveConference( + absl::string_view join_endpoint, absl::string_view conference_id, + absl::string_view access_token) = 0; + + // Convenience method for sending a `SessionControlChannelFromClient` request + // with a `LeaveRequest` to Meet servers. This tells the server that the + // client should be disconnected from the conference. The request will use the + // provided request ID. See `SendRequest` for more information. + // + // If successful, the client will receive a `SessionControlChannelToClient` + // resource update with the same request ID, a session status of + // SESSION_DISCONNECTED, and a `LeaveResponse`. + // + // If this is called before the client is fully joined, the client will + // immediately transition to the Disconnected state, as the Meet servers will + // not necessarily respond to the request until the client is fully joined. + virtual absl::Status LeaveConference(int64_t request_id) = 0; + + // Sends a resource request to Meet servers. + // + // Requests must have a non-zero, unique `request_id` in the nested request. + // For example, a `SessionControlRequest`'s `request_id` must be non-zero and + // unique to other requests' `request_id`s. The `request_id` can be used to + // associate the request to the response or error in the + // `MediaApiClientObserverInterface`. + virtual absl::Status SendRequest(const ResourceRequest& request) = 0; + + // Creates a new instance of MediaApiClientInterface. + // + // It is configured with the required codecs to support streaming media from + // Meet conferences. Required SCTP data channels will be opened and the proper + // number of SRTP streams will be signaled with Meet servers. + // + // The observer will be retained by the client until the client is + // destroyed. + static absl::StatusOr> Create( + const MediaApiClientConfiguration& api_config, + absl::Nonnull> + api_session_observer); +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_MEDIA_API_CLIENT_INTERFACE_H_ diff --git a/native_with_state/api/media_entries_resource.h b/native_with_state/api/media_entries_resource.h new file mode 100644 index 0000000..a825c22 --- /dev/null +++ b/native_with_state/api/media_entries_resource.h @@ -0,0 +1,118 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_MEDIA_ENTRIES_RESOURCE_H_ +#define NATIVE_WITH_STATE_API_MEDIA_ENTRIES_RESOURCE_H_ + +#include +#include +#include +#include + +// TODO: Update the docs for all the resource structs in this file +// and make it clear how resources are used. I.e. what each update is and how a +// client can/should react to them. + +namespace meet { + +struct MediaEntry { + // Participant resource name, not display name. There is a many + // (participant) to one (media entry) relationship. + // See + // https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.participants + // for more info. + // + // Format is + // `conferenceRecords/{conference_record}/participants/{participant}` Use this + // to correlate with other media entries produced by the same participant. + // For example, a participant with multiple devices active in the same + // conference. + // Unused for now. + std::optional participant; + // Participant key of associated participant. The user must construct the + // resource name from this field to create a Meet API reference. + // + // Format is`participants/{participant}` + // + // You can retrieve the conference record from + // https://developers.google.com/meet/api/guides/conferences and use the + // conference record to construct the participant name in the format of + // `conferenceRecords/{conference_record}/participants/{participant}` + std::optional participant_key; + // Participant session name. There should be a one to one mapping of session + // to Media Entry. See + // https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.participants.participantSessions + // for more info. + // + // Format is + // `conferenceRecords/{conference_record}/participants/{participant}/participantSessions/{participant_session}` + // Unused for now. + std::optional session; + // The session id of the media entry. The user must construct the + // session name from this field to create an Meet API reference. + // This can be done by combining the conference record, participant key, and + // session id. + // + // Format is + // `participants/{participant}/participantSessions/{participant_session}` + // + // You can retrieve the conference record from + // https://developers.google.com/meet/api/guides/conferences and use the + // conference record to construct the participant name in the format of + // `conferenceRecords/{conference_record}/participants/{participant}` + std::optional session_name; + // The CSRC for any audio stream contributed by this participant. Will be + // zero if no stream is provided. + uint32_t audio_csrc = 0; + // The CSRC for any video stream contributed by this participant. Will be + // empty if no stream is provided. + std::vector video_csrcs; + // Signals if the current entry is presenting. + bool presenter = false; + // Signals if the current entry is a screenshare. + bool screenshare = false; + // Signals if the audio stream is currently muted by the remote participant. + bool audio_muted = false; + // Signals if the video stream is currently muted by the remote participant. + bool video_muted = false; +}; + +struct MediaEntriesResourceSnapshot { + // The resource ID of the resource being updated. + int64_t id = 0; + std::optional media_entry; +}; + +struct MediaEntriesDeletedResource { + // The resource ID of the resource being deleted. + int64_t id = 0; + std::optional media_entry; +}; + +// The top-level transport container for messages sent from server to +// client in the "media-entries" data channel. +struct MediaEntriesChannelToClient { + // Resource snapshots. There is no implied order between the snapshots in the + // list. + std::vector resources; + // The list of deleted resources. There is no order between the entries in the + // list. + std::vector deleted_resources; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_MEDIA_ENTRIES_RESOURCE_H_ diff --git a/native_with_state/api/media_stats_resource.h b/native_with_state/api/media_stats_resource.h new file mode 100644 index 0000000..65addce --- /dev/null +++ b/native_with_state/api/media_stats_resource.h @@ -0,0 +1,121 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_MEDIA_STATS_RESOURCE_H_ +#define NATIVE_WITH_STATE_API_MEDIA_STATS_RESOURCE_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/types/optional.h" + +// TODO: Update the docs for all the resource structs in this file +// and make it clear how resources are used. I.e. what each update is and how a +// client can/should react to them. + +namespace meet { + +struct MediaStatsResponse { + struct UploadMediaStatsResponse {}; + + int64_t request_id = 0; + // The response status from Meet servers to an incoming request. This should + // be used by clients to determine the outcome of the request. + absl::Status status; + std::optional upload_media_stats; +}; + +// The configuration for the media stats upload. This will be sent by the server +// to the client when the data channel is opened. The client is then expected to +// start uploading media stats at the specified interval. +// +// This configuration is immutable and a singleton and will only be sent once +// when the data channel is opened. +struct MediaStatsConfiguration { + // The interval between each upload of media stats. If this is zero, the + // client should not upload any media stats. + int32_t upload_interval_seconds = 0; + // A map of allowlisted RTCStats sections. The key is the section type, and + // the value is a vector of the names of data that are allowlisted for that + // section. + // + // Allowlisted sections and section data are expected to be uploaded by the + // client. Other data will be ignored by the server and can be safely + // omitted. + absl::flat_hash_map> allowlist; +}; + +// A resource snapshot managed by the server and replicated to the client. +struct MediaStatsResourceSnapshot { + int64_t id = 0; + MediaStatsConfiguration configuration; +}; + +// The top-level transport container for messages sent from server to client in +// the "media-stats" data channel. Any combination of fields may be set, but the +// message is never empty. +struct MediaStatsChannelToClient { + // An optional response to an incoming request. + std::optional response; + // Resource snapshots. + std::optional> resources; +}; + +// This type represents an RTCStats-derived dictionary described in +// https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats which is +// returned by calling `RTCPeerConnection::getStats`. +struct MediaStatsSection { + // The RTCStatsType of the section. E.g. "codec", "candidate-pair", etc. + // See: https://www.w3.org/TR/webrtc-stats/#rtcstatstype-str*. + std::string type; + // The WebRTC-generated id of the section. + std::string id; + // The stats and their values for this section. + // https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats. + absl::flat_hash_map values; +}; + +// Uploads media stats from the client to the server. The stats are retrieved +// from WebRTC by calling `RTCPeerConnection::getStats` and the returned +// RTCStatsReport can be easily mapped to the sections below. +struct UploadMediaStatsRequest { + // Represents the entries in + // https://w3c.github.io/webrtc-pc/#dom-rtcstatsreport. + std::vector sections; +}; + +struct MediaStatsRequest { + // A unique client-generated identifier for this request. Different requests + // must never have the same request ID. + int64_t request_id = 0; + // Request payload. + std::optional upload_media_stats; +}; + +// The top-level transport container for messages sent from client to server in +// the "media-stats" data channel. +struct MediaStatsChannelFromClient { + MediaStatsRequest request; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_MEDIA_STATS_RESOURCE_H_ diff --git a/native_with_state/api/participants_resource.h b/native_with_state/api/participants_resource.h new file mode 100644 index 0000000..6f2534f --- /dev/null +++ b/native_with_state/api/participants_resource.h @@ -0,0 +1,122 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_PARTICIPANTS_RESOURCE_H_ +#define NATIVE_WITH_STATE_API_PARTICIPANTS_RESOURCE_H_ + +#include +#include +#include +#include + +namespace meet { + +// Signed in user type, always has a unique id and display name. +struct SignedInUser { + // Unique ID for the user. Interoperable with Admin SDK API and People API. + // Format: `users/{user}` + std::string user; + // For a personal device, it's the user's first name and last name. + // For a robot account, it's the administrator-specified device name. For + // example, "Altostrat Room". + std::string display_name; +}; + +// Anonymous user. +struct AnonymousUser { + // User provided name when they join a conference anonymously. + std::string display_name; +}; + +// Phone user, always has a display name. User dialing in from a phone where the +// user's identity is unknown because they haven't signed in with a Google +// Account. +struct PhoneUser { + // Partially redacted user's phone number when calling. + std::string display_name; +}; + +struct Participant { + enum class Type { kSignedInUser = 0, kAnonymousUser = 1, kPhoneUser = 2 }; + + // Numeric ID for the participant. + // + // Will eventually be deprecated in favor of `name`. + int32_t participant_id; + + // Participant resource name, not display name. There is a many + // (participant) to one (media entry) relationship. + // See + // https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.participants + // for more info. + // + // Format is + // `conferenceRecords/{conference_record}/participants/{participant}`. Use + // this to correlate with other media entries produced by the same + // participant. For example, a participant with multiple devices active in the + // same conference. Unused for now. + std::optional name; + + // Participant key of associated participant. The user must construct the + // resource name from this field to create a Meet API reference. + // + // Format is`participants/{participant}` + // + // You can retrieve the conference record from + // https://developers.google.com/meet/api/guides/conferences and use the + // conference record to construct the participant name in the format of + // `conferenceRecords/{conference_record}/participants/{participant}` + std::optional participant_key; + // The type of participant. + // + // This is used to determine which of the following fields are populated. + Type type; + std::optional signed_in_user; + std::optional anonymous_user; + std::optional phone_user; +}; + +// A resource snapshot managed by the server and replicated to the client. +struct ParticipantResourceSnapshot { + // The resource ID of the resource being updated. + int64_t id; + + std::optional participant; +}; + +struct ParticipantDeletedResource { + // The resource ID of the resource being deleted. + int64_t id; + + // The type of resource being deleted. + std::optional participant; +}; + +// The top-level transport container for messages converted from proto to C++ +// struct. +struct ParticipantsChannelToClient { + // Resource snapshots. There is no implied order between the snapshots in the + // list. + std::vector resources; + + // The list of deleted resources. There is no order between the entries in + // the list. + std::vector deleted_resources; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_PARTICIPANTS_RESOURCE_H_ diff --git a/native_with_state/api/session_control_resource.h b/native_with_state/api/session_control_resource.h new file mode 100644 index 0000000..4e5b289 --- /dev/null +++ b/native_with_state/api/session_control_resource.h @@ -0,0 +1,99 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_SESSION_CONTROL_RESOURCE_H_ +#define NATIVE_WITH_STATE_API_SESSION_CONTROL_RESOURCE_H_ + +#include +#include +#include + +#include "absl/status/status.h" + +// TODO: Update the docs for all the resource structs in this file +// and make it clear how resources are used. I.e. what each update is and how a +// client can/should react to them. + +namespace meet { + +// Tells the server that the client is about to disconnect. +// +// See MeetMediaApiClientInterface `LeaveConference` method for more +// information. +struct LeaveRequest {}; + +struct SessionControlRequest { + int64_t request_id = 0; + std::optional leave_request; +}; + +// The top-level transport container for messages sent from client to +// server in the "session-control" data channel. Any combination of fields may +// be set, but the message is never empty. +struct SessionControlChannelFromClient { + SessionControlRequest request; +}; + +// This is a singleton resource containing the status of the media session. +struct SessionStatus { + enum class ConferenceConnectionState { + kUnknown, + // Session is waiting to be admitted into the conference. + // The client may never observe this state if it was admitted or rejected + // quickly. + kWaiting, + // Session has fully joined the conference. + kJoined, + // Session is not connected to the conference. + // + // This will be sent from the server when the client is no longer connected + // to the conference. This can occur for a variety of reasons, including the + // client being kicked from the conference, the client not being admitted + // into the conference, or the conference ending. + kDisconnected, + }; + + ConferenceConnectionState connection_state = + ConferenceConnectionState::kUnknown; +}; + +struct SessionControlResourceSnapshot { + int64_t id; + SessionStatus session_status; +}; + +struct LeaveResponse {}; + +// An optional response from Meet servers to an incoming request. +struct SessionControlResponse { + int64_t request_id; + // The response status from Meet servers to an incoming request. This should + // be used by clients to determine the outcome of the request. + absl::Status status; + LeaveResponse leave_response; +}; + +// The top-level transport container for messages sent from server to +// client in the "session-control" data channel. Any combination of fields may +// be set, but the message is never empty. +struct SessionControlChannelToClient { + std::optional response; + std::vector resources; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_SESSION_CONTROL_RESOURCE_H_ diff --git a/native_with_state/api/video_assignment_resource.h b/native_with_state/api/video_assignment_resource.h new file mode 100644 index 0000000..ba68a56 --- /dev/null +++ b/native_with_state/api/video_assignment_resource.h @@ -0,0 +1,157 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVE_WITH_STATE_API_VIDEO_ASSIGNMENT_RESOURCE_H_ +#define NATIVE_WITH_STATE_API_VIDEO_ASSIGNMENT_RESOURCE_H_ + +#include +#include +#include +#include + +#include "absl/status/status.h" + +// TODO: Update the docs for all the resource structs in this file +// and make it clear how resources are used. I.e. what each update is and how a +// client can/should react to them. + +namespace meet { + +// Required dimensions of the canvas. +struct CanvasDimensions { + // The vertical space, in pixels, for this canvas. + int32_t height = 480; + // The horizontal space, in pixels, for this canvas. + int32_t width = 640; +}; + +struct VideoCanvas { + enum class AssignmentProtocol { + kRelevant, + kDirect, + }; + // An identifier for the video canvas. + // This is required and must be unique within the containing LayoutModel. + // Clients should prudently reuse VideoCanvas IDs. This allows the backend + // to keep assigning video streams to the same canvas as much as possible. + int32_t id = 0; + + // The dimensions for this video canvas. Failure to provide this + // will result in an error. + CanvasDimensions dimensions; + + // The protocol that governs how the backend should assign a video + // feed to this canvas. + AssignmentProtocol assignment_protocol = AssignmentProtocol::kRelevant; +}; + +struct LayoutModel { + // A client-specified identifier for this assignment. The identifier + // will be used to reference a given LayoutModel in subsequent + // VideoAssignment resource update pushed from server -> client. + std::string label; + + // The canvases that videos are assigned to from each virtual ssrc. + // Providing more canvases than exists virtual streams will result in + // an error status. + std::vector canvases; +}; + +struct VideoResolution { + // The height and width are in square pixels. For cameras that can change + // orientation, the width refers to the measurement on the horizontal axis, + // and the height on the vertical. + int32_t height = 480; + int32_t width = 640; + // The frame rate in frames per second. + int32_t frame_rate = 30; +}; + +struct SetVideoAssignmentRequest { + // The new video layout to use. This replaces any previously active video + // layout. + LayoutModel layout_model; + // The maximum video resolution the client wants to receive for any video + // feed. + VideoResolution video_resolution; +}; + +struct VideoAssignmentRequest { + // A unique client-generated identifier for this request. Different requests + // must never have the same request ID. + int64_t request_id = 0; + + std::optional set_video_assignment_request; +}; + +// The top-level transport container for messages sent from client to +// server in the "video-assignment" data channel. +struct VideoAssignmentChannelFromClient { + VideoAssignmentRequest request; +}; + +struct VideoAssignmentResponse { + struct SetVideoAssignmentResponse {}; + + // The request ID in the request this is the response to. + int64_t request_id = 0; + // The response status for this request. This should be used by clients to + // determine the RPC result. + absl::Status status; + std::optional set_assignment; +}; + +struct VideoCanvasAssignment { + // The video canvas the video should be shown in. + int32_t canvas_id = 0; + // The virtual video SSRC that the video will be sent over, or zero if + // there is no video from the participant. + uint32_t ssrc = 0; + // The `MediaEntry.id` of the media whose video is being shown. + int32_t media_entry_id = 0; +}; + +struct VideoAssignment { + // The LayoutModel that this assignment is based on. Taken from the + // LayoutModel.label field. + std::string label; + // The individual canvas assignments, in no particular order. + std::vector canvases; +}; + +// A resource snapshot managed by the server and replicated to the client. +struct VideoAssignmentResourceSnapshot { + // The resource ID of the resource being updated. For singleton + // resources, this is zero. + int64_t id = 0; + + std::optional assignment; +}; + +// The top-level transport container for messages sent from server to +// client in the "video-assignment" data channel. Any combination of fields may +// be set, but the message is never empty. +struct VideoAssignmentChannelToClient { + // An optional response to a incoming request. + std::optional response; + // Resource snapshots. There is no implied order between the snapshots in the + // list. + std::vector resources; +}; + +} // namespace meet + +#endif // NATIVE_WITH_STATE_API_VIDEO_ASSIGNMENT_RESOURCE_H_ diff --git a/web/types/BUILD b/web/types/BUILD index 2824c09..18c23bf 100644 --- a/web/types/BUILD +++ b/web/types/BUILD @@ -13,7 +13,7 @@ # limitations under the License. load( - "//javascript/typescript:build_defs.bzl", + "//:build_defs.bzl", "ts_library", ) diff --git a/web/utils/BUILD b/web/utils/BUILD deleted file mode 100644 index 6357f49..0000000 --- a/web/utils/BUILD +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -load("@rules_python//python:py_binary.bzl", "py_binary") -load("@util_deps//:requirements.bzl", "requirement") - -py_binary( - name = "get_meeting_space_id", - srcs = ["get_meeting_space_id.py"], - python_version = "PY3", - deps = [ - requirement("google-apps-meet"), - requirement("google-auth"), - requirement("google-auth-oauthlib"), - requirement("grpcio"), - "@com_google_absl_py//absl:app", - "@com_google_absl_py//absl/flags", - ], -) - -py_binary( - name = "get_meeting_space_id_service_account", - srcs = ["get_meeting_space_id_service_account.py"], - python_version = "PY3", - deps = [ - requirement("google-apps-meet"), - requirement("google-auth"), - requirement("google-auth-oauthlib"), - requirement("grpcio"), - "@com_google_absl_py//absl:app", - "@com_google_absl_py//absl/flags", - ], -) - -py_binary( - name = "generate_service_account_access_token", - srcs = ["generate_service_account_access_token.py"], - python_version = "PY3", - deps = [ - requirement("google-auth"), - requirement("requests"), - "@com_google_absl_py//absl:app", - "@com_google_absl_py//absl/flags", - ], -) - -filegroup( - name = "requirements_file", - srcs = ["requirements.txt"], -) diff --git a/web/utils/generate_service_account_access_token.py b/web/utils/generate_service_account_access_token.py deleted file mode 100644 index b57d2e9..0000000 --- a/web/utils/generate_service_account_access_token.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Generates an access token for a service account. - -This script generates an access token for a service account by exchanging a JWT -for an access token. The JWT is signed with the private key of the service -account. The access token can then be used to make authenticated requests to -Google APIs. - -For more information: -https://developers.google.com/identity/protocols/oauth2/service-account -""" - -import datetime -import json -from typing import Sequence - -from absl import app -from absl import flags -import google.auth.crypt -import google.auth.jwt -import requests - -SCOPES = "https://www.googleapis.com/auth/meetings.space.readonly,https://www.googleapis.com/auth/meetings.media.readonly,https://www.googleapis.com/auth/meetings.space.created" - -_SERVICE_ACCOUNT_EMAIL = flags.DEFINE_string( - "service_account_email", - None, - "Service account email", -) - -_DELEGATE_EMAIL = flags.DEFINE_string( - "delegate_email", - None, - "Delegate email that service account impersonates", -) - -_PRIVATE_KEY_FILE_PATH = flags.DEFINE_string( - "private_key_file_path", - "./credentials.json", - "Path to private key file", -) - -_SCOPES = flags.DEFINE_string( - "scopes", SCOPES, "String of oauth scopes separated by commas" -) - - -def generate_jwt_and_access_token( - service_account_email_arg, - private_key_file_arg, - delegate_email_arg, - scopes_arg, -): - """Generates a JWT and then an access token. - - Args: - service_account_email_arg: The email address of the service account. - private_key_file_arg: The path to the private key file. - delegate_email_arg: The email to delegate access to. - scopes_arg: string of oauth scopes separated by commas. - - Returns: - The access token. - - Raises: - Exception: If the request to exchange the JWT for an access token fails. - """ - - scopes = scopes_arg.split(",") - # Load the private key - with open(private_key_file_arg, "r") as f: - private_key = f.read() - - file_key = json.loads(private_key) - # Create the JWT header - header = {"alg": "RS256", "typ": "JWT"} - - # Create the JWT payload - now = datetime.datetime.now(datetime.timezone.utc) - exp = now + datetime.timedelta(hours=1) # Token expires in 1 hour - payload = { - "iss": service_account_email_arg, - "sub": ( - delegate_email_arg - if delegate_email_arg - else service_account_email_arg - ), - "aud": "https://oauth2.googleapis.com/token", - "scope": " ".join(scopes), - "iat": int(now.timestamp()), - "exp": int(exp.timestamp()), - } - - # Sign the JWT with the private key - signer = google.auth.crypt.RSASigner.from_string(file_key["private_key"]) - jwt = google.auth.jwt.encode(signer, payload, header) - - # Exchange the JWT for an access token - data = { - "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", - "assertion": jwt, - } - response = requests.post("https://oauth2.googleapis.com/token", data=data) - # Handle potential errors - if response.status_code != 200: - # pylint: disable=broad-exception-raised - raise Exception(f"Error getting access token: {response.text}") - - with open("service_account_token.json", "w") as token: - token.write(json.dumps(response.json())) - - # Extract the access token - oauth2_access_token = response.json()["access_token"] - return oauth2_access_token - - -def main(argv: Sequence[str]): - """Shows basic usage of the Google Meet API.""" - del argv - service_account_email = _SERVICE_ACCOUNT_EMAIL.value - delegate_email = _DELEGATE_EMAIL.value - private_key_file = _PRIVATE_KEY_FILE_PATH.value - scopes = _SCOPES.value - if not service_account_email: - # pylint: disable=broad-exception-raised - raise Exception("service account is required") - if not delegate_email: - # pylint: disable=broad-exception-raised - raise Exception("delegate email is required") - - try: - access_token = generate_jwt_and_access_token( - service_account_email, private_key_file, delegate_email, scopes - ) - print("Access Token:", access_token) - # pylint: disable=broad-except - except Exception as e: - print(f"An error occurred: {e}") - - -if __name__ == "__main__": - app.run(main) diff --git a/web/utils/get_meeting_space_id.py b/web/utils/get_meeting_space_id.py deleted file mode 100644 index 886b6fe..0000000 --- a/web/utils/get_meeting_space_id.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Gets meeting space id from meeting space code. - -Modified from -https://developers.google.com/meet/api/guides/quickstart/python#configure_the_sample -and -https://developers.google.com/meet/api/guides/meeting-spaces#get-meeting-space. -Note that this has the limitation that it has to be run in an environment that -can open a browser. -""" - -import os.path -from typing import Sequence - -from absl import app -from absl import flags -from google.apps import meet_v2 -from google.auth.transport.requests import Request -from google.oauth2.credentials import Credentials -from google_auth_oauthlib.flow import InstalledAppFlow - - -# If modifying these scopes, delete the file token.json. -SCOPES = 'https://www.googleapis.com/auth/meetings.space.readonly,https://www.googleapis.com/auth/meetings.media.readonly' - -_MEETING_CODE = flags.DEFINE_string( - 'meeting_code', None, 'Meeting code of your meeting' -) -_CREDENTIAL_FILE_PATH = flags.DEFINE_string( - 'credential_file_path', - './credentials.json', - 'Path to client credential file', -) -_SCOPES = flags.DEFINE_string( - 'scopes', SCOPES, 'String of oauth scopes separated by commas' -) - - -def main(argv: Sequence[str]): - """Shows basic usage of the Google Meet API.""" - del argv - creds = None - # The file token.json stores the user's access and refresh tokens, and is - # created automatically when the authorization flow completes for the first - # time. - scopes = _SCOPES.value.split(',') - if not _MEETING_CODE.value: - print('meeting code string is required') - return - - if os.path.exists('token.json'): - creds = Credentials.from_authorized_user_file('token.json', scopes) - # If there are no (valid) credentials available, let the user log in. - if not creds or not creds.valid: - if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - else: - flow = InstalledAppFlow.from_client_secrets_file( - _CREDENTIAL_FILE_PATH.value, scopes - ) - creds = flow.run_local_server(port=0) - # Save the credentials for the next run - with open('token.json', 'w') as token: - token.write(creds.to_json()) - - print(creds.token) - - try: - client = meet_v2.SpacesServiceClient(credentials=creds) - request = meet_v2.GetSpaceRequest(name=f'spaces/{_MEETING_CODE.value}') - response = client.get_space(request=request) - print(f'Meeting Space Id: {response.name}') - # pylint: disable=broad-except - except Exception as error: - print(f'An error occurred: {error}') - - -if __name__ == '__main__': - app.run(main) diff --git a/web/utils/get_meeting_space_id_service_account.py b/web/utils/get_meeting_space_id_service_account.py deleted file mode 100644 index 409c4eb..0000000 --- a/web/utils/get_meeting_space_id_service_account.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Gets meeting space id from meeting space code. - -Modified from -https://developers.google.com/meet/api/guides/quickstart/python#configure_the_sample -and -https://developers.google.com/meet/api/guides/meeting-spaces#get-meeting-space. -""" - -from typing import Sequence -from absl import app -from absl import flags -from google.apps import meet_v2 -from google.oauth2 import service_account - - -# If modifying these scopes, delete the file token.json. -SCOPES = 'https://www.googleapis.com/auth/meetings.space.readonly' - -_SCOPES = flags.DEFINE_string( - 'scopes', SCOPES, 'String of oauth scopes separated by commas' -) - -_MEETING_CODE = flags.DEFINE_string( - 'meeting_code', None, 'Meeting code of your meeting' -) - -_PRIVATE_KEY_FILE_PATH = flags.DEFINE_string( - 'private_key_file_path', - './credentials.json', - 'Path to private key file', -) - -_DELEGATE_EMAIL = flags.DEFINE_string( - 'delegate_email', - None, - 'Delegate email that service account impersonates', -) - - -def get_meeting_space_id( - meeting_code_arg, - private_key_file_arg, - delegate_email_arg, - scopes_arg, -): - """Get meeting space with service account. - - Args: - meeting_code_arg: The meeting space code for target meeting - private_key_file_arg: The path to the private key file. - delegate_email_arg: The email to delegate access to. - scopes_arg: string of oauth scopes separated by commas. - - Returns: - The meeting space id - - Raises: - Exception: If the request for meeting id fails. - """ - scopes = scopes_arg.split(',') - - creds = service_account.Credentials.from_service_account_file( - private_key_file_arg, scopes=scopes, subject=delegate_email_arg - ) - - try: - client = meet_v2.SpacesServiceClient(credentials=creds) - request = meet_v2.GetSpaceRequest(name=f'spaces/{meeting_code_arg}') - response = client.get_space(request=request) - print(f'Meeting Space Id: {response.name}') - # pylint: disable=broad-except - except Exception as error: - print(f'An error occurred: {error}') - - -def main(argv: Sequence[str]): - """Shows basic usage of the Google Meet API.""" - del argv - meeting_code = _MEETING_CODE.value - private_key_file = _PRIVATE_KEY_FILE_PATH.value - scopes = _SCOPES.value - delegate_email = _DELEGATE_EMAIL.value - if not meeting_code: - # pylint: disable=broad-exception-raised - raise Exception('meeting code is required') - if not delegate_email: - # pylint: disable=broad-exception-raised - raise Exception('delegate email is required') - get_meeting_space_id(meeting_code, private_key_file, delegate_email, scopes) - - -if __name__ == '__main__': - app.run(main) diff --git a/web/utils/requirements.txt b/web/utils/requirements.txt deleted file mode 100644 index a5c14f8..0000000 --- a/web/utils/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -cachetools==5.5.0 -certifi==2024.7.4 -charset-normalizer==3.3.2 -google-api-core==2.19.1 -google-apps-meet==0.1.8 -google-auth==2.34.0 -google-auth-httplib2==0.2.0 -google-auth-oauthlib==1.2.1 -googleapis-common-protos==1.63.2 -grpcio==1.65.5 -grpcio-status==1.65.5 -httplib2==0.22.0 -idna==3.7 -oauthlib==3.2.2 -proto-plus==1.24.0 -protobuf==5.27.3 -pyasn1==0.6.0 -pyasn1_modules==0.4.0 -pyparsing==3.1.2 -requests==2.32.3 -requests-oauthlib==2.0.0 -rsa==4.9 -urllib3==2.2.2 -absl-py==2.1.0