| // Copyright 2023 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "quiche/quic/moqt/moqt_session.h" | |
| #include <array> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <variant> | |
| #include "absl/base/casts.h" | |
| #include "absl/base/nullability.h" | |
| #include "absl/container/btree_map.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/container/node_hash_map.h" | |
| #include "absl/memory/memory.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "quiche/quic/core/quic_alarm_factory.h" | |
| #include "quiche/quic/core/quic_time.h" | |
| #include "quiche/quic/core/quic_types.h" | |
| #include "quiche/quic/moqt/moqt_bidi_stream.h" | |
| #include "quiche/quic/moqt/moqt_error.h" | |
| #include "quiche/quic/moqt/moqt_fetch_stream.h" | |
| #include "quiche/quic/moqt/moqt_fetch_task.h" | |
| #include "quiche/quic/moqt/moqt_framer.h" | |
| #include "quiche/quic/moqt/moqt_key_value_pair.h" | |
| #include "quiche/quic/moqt/moqt_live_publisher.h" | |
| #include "quiche/quic/moqt/moqt_messages.h" | |
| #include "quiche/quic/moqt/moqt_names.h" | |
| #include "quiche/quic/moqt/moqt_namespace_stream.h" | |
| #include "quiche/quic/moqt/moqt_object.h" | |
| #include "quiche/quic/moqt/moqt_object_subscriber.h" | |
| #include "quiche/quic/moqt/moqt_parser.h" | |
| #include "quiche/quic/moqt/moqt_priority.h" | |
| #include "quiche/quic/moqt/moqt_publish_namespace_stream.h" | |
| #include "quiche/quic/moqt/moqt_publish_stream.h" | |
| #include "quiche/quic/moqt/moqt_publisher.h" | |
| #include "quiche/quic/moqt/moqt_session_callbacks.h" | |
| #include "quiche/quic/moqt/moqt_session_interface.h" | |
| #include "quiche/quic/moqt/moqt_subscribe_stream.h" | |
| #include "quiche/quic/moqt/moqt_track_status_stream.h" | |
| #include "quiche/quic/moqt/moqt_types.h" | |
| #include "quiche/quic/moqt/moqt_uni_stream.h" | |
| #include "quiche/quic/platform/api/quic_logging.h" | |
| #include "quiche/common/platform/api/quiche_bug_tracker.h" | |
| #include "quiche/common/platform/api/quiche_logging.h" | |
| #include "quiche/common/quiche_buffer_allocator.h" | |
| #include "quiche/common/quiche_callbacks.h" | |
| #include "quiche/common/quiche_mem_slice.h" | |
| #include "quiche/common/quiche_weak_ptr.h" | |
| #include "quiche/web_transport/web_transport.h" | |
| #define ENDPOINT \ | |
| (perspective() == Perspective::IS_SERVER ? "MoQT Server: " : "MoQT Client: ") | |
| namespace moqt { | |
| namespace { | |
| using ::quic::Perspective; | |
| class DefaultPublisher : public MoqtPublisher { | |
| public: | |
| static DefaultPublisher* GetInstance() { | |
| static DefaultPublisher* instance = new DefaultPublisher(); | |
| return instance; | |
| } | |
| // MoqtPublisher implementation. | |
| absl_nullable std::shared_ptr<MoqtTrackPublisher> GetTrack( | |
| const FullTrackName& track_name) override { | |
| QUICHE_DCHECK(track_name.IsValid()); | |
| return nullptr; | |
| } | |
| }; | |
| } // namespace | |
| MoqtSession::MoqtSession(webtransport::Session* session, | |
| MoqtSessionParameters parameters, | |
| std::unique_ptr<quic::QuicAlarmFactory> alarm_factory, | |
| MoqtSessionCallbacks callbacks) | |
| : session_(session), | |
| parameters_(parameters), | |
| callbacks_(std::move(callbacks)), | |
| framer_(parameters.using_webtrans, parameters.perspective), | |
| publisher_(DefaultPublisher::GetInstance()), | |
| alarm_factory_(std::move(alarm_factory)), | |
| weak_ptr_factory_(this), | |
| weak_ptr_factory_for_publishers_(this), | |
| liveness_token_(std::make_shared<Empty>()) { | |
| if (parameters_.using_webtrans) { | |
| session_->SetOnDraining([this]() { | |
| QUICHE_DLOG(INFO) << "WebTransport session is draining"; | |
| if (callbacks_.goaway_received_callback == nullptr) { | |
| return; | |
| } | |
| MoqtSessionGoAwayCallback callback = | |
| std::move(callbacks_.goaway_received_callback); | |
| callbacks_.goaway_received_callback = nullptr; | |
| std::move(callback)(absl::string_view(), quic::QuicTimeDelta::Zero()); | |
| }); | |
| } | |
| if (parameters_.perspective == Perspective::IS_SERVER) { | |
| next_request_id_ = 1; | |
| next_incoming_request_id_ = 0; | |
| } | |
| QUICHE_DCHECK(parameters_.moqt_implementation.empty()); | |
| parameters_.moqt_implementation = kImplementationName; | |
| } | |
| void MoqtSession::SendControlMessage(quiche::QuicheBuffer message) { | |
| OutgoingControlStream* control_stream = GetOutgoingControlStream(); | |
| if (control_stream == nullptr) { | |
| QUICHE_LOG(DFATAL) << "Trying to send a message on the control stream " | |
| "while it does not exist"; | |
| return; | |
| } | |
| control_stream->SendOrBufferMessageOrFatal(std::move(message)); | |
| } | |
| void MoqtSession::OnSessionReady() { | |
| QUICHE_DLOG(INFO) << ENDPOINT << "Underlying session ready"; | |
| std::optional<std::string> version = session_->GetNegotiatedSubprotocol(); | |
| if (version != parameters_.version) { | |
| Error(MoqtError::kVersionNegotiationFailed, | |
| "MOQT peer chose wrong subprotocol"); | |
| return; | |
| } | |
| if (!session_->CanOpenNextOutgoingUnidirectionalStream()) { | |
| Error(MoqtError::kControlMessageTimeout, "Unable to open a control stream"); | |
| return; | |
| } | |
| webtransport::Stream* stream = session_->OpenOutgoingUnidirectionalStream(); | |
| if (stream == nullptr) { | |
| Error(MoqtError::kInternalError, "Unable to open a control stream"); | |
| return; | |
| } | |
| auto control_stream = std::make_unique<OutgoingControlStream>(this, stream); | |
| outgoing_control_stream_ = control_stream->GetWeakPtr(); | |
| trace_recorder_.RecordControlStreamCreated(stream->GetStreamId()); | |
| stream->SetVisitor(std::move(control_stream)); | |
| MoqtSetup setup; | |
| parameters_.ToSetupOptions(setup.options); | |
| SendControlMessage(framer_.SerializeSetup(setup)); | |
| QUIC_DLOG(INFO) << ENDPOINT << "Send SETUP"; | |
| } | |
| void MoqtSession::OnSessionClosed(webtransport::SessionErrorCode, | |
| const std::string& error_message) { | |
| if (!error_.empty()) { | |
| // Avoid erroring out twice. | |
| return; | |
| } | |
| QUICHE_DLOG(INFO) << ENDPOINT << "Underlying session closed with message: " | |
| << error_message; | |
| error_ = error_message; | |
| CleanUpState(); | |
| std::move(callbacks_.session_terminated_callback)(error_message); | |
| } | |
| void MoqtSession::OnIncomingBidirectionalStreamAvailable() { | |
| if (!peer_setup_received_) { | |
| return; | |
| } | |
| while (webtransport::Stream* stream = | |
| session_->AcceptIncomingBidirectionalStream()) { | |
| if (sent_goaway_) { | |
| // Immediately reject new requests with REQUEST_ERROR. If the stream | |
| // cannot be written, just reset it. | |
| if (!stream->CanWrite()) { | |
| stream->SendStopSending(kResetCodeGoingAway); | |
| stream->ResetWithUserCode(kResetCodeGoingAway); | |
| continue; | |
| } | |
| webtransport::StreamWriteOptions options; | |
| options.set_send_fin(true); | |
| std::array write_vector = { | |
| quiche::QuicheMemSlice(framer_.SerializeRequestError(MoqtRequestError{ | |
| RequestErrorCode::kGoingAway, std::nullopt, ""}))}; | |
| if (!stream->Writev(absl::MakeSpan(write_vector), options).ok()) { | |
| stream->SendStopSending(kResetCodeGoingAway); | |
| stream->ResetWithUserCode(kResetCodeGoingAway); | |
| }; | |
| continue; | |
| } | |
| auto bidi_stream = std::make_unique<UnknownBidiStream>(this, stream); | |
| stream->SetVisitor(std::move(bidi_stream)); | |
| stream->visitor()->OnCanRead(); | |
| } | |
| } | |
| void MoqtSession::OnIncomingUnidirectionalStreamAvailable() { | |
| while (webtransport::Stream* stream = | |
| session_->AcceptIncomingUnidirectionalStream()) { | |
| stream->SetVisitor(std::make_unique<UnknownUniStream>(this, stream)); | |
| stream->visitor()->OnCanRead(); | |
| } | |
| } | |
| void MoqtSession::OnDatagramReceived(absl::string_view datagram) { | |
| MoqtObject message; | |
| bool use_default_priority; | |
| absl::StatusOr<absl::string_view> payload = | |
| ParseDatagram(datagram, message, use_default_priority); | |
| if (!payload.ok()) { | |
| if (absl::IsNotFound(payload.status())) { | |
| return; // It's PADDING. | |
| } | |
| Error(MoqtError::kProtocolViolation, "Malformed datagram received"); | |
| return; | |
| } | |
| QUICHE_DLOG(INFO) << ENDPOINT | |
| << "Received OBJECT message in datagram for request_id " | |
| << " for track alias " << message.track_alias | |
| << " with sequence " << message.group_id << ":" | |
| << message.object_id << " priority " | |
| << message.publisher_priority << " length " | |
| << payload->size(); | |
| LiveSubscriber* track = SubscribeByAlias(message.track_alias); | |
| if (track == nullptr) { | |
| return; | |
| } | |
| track->OnObjectOrOk(); | |
| if (use_default_priority) { | |
| message.publisher_priority = track->default_publisher_priority(); | |
| } | |
| if (!track->InWindow(Location(message.group_id, message.object_id))) { | |
| // TODO(martinduke): a recent REQUEST_UPDATE could put us here, and it's | |
| // not an error. | |
| return; | |
| } | |
| QUICHE_CHECK(!track->is_fetch()); | |
| SubscribeVisitor* visitor = track->visitor(); | |
| if (visitor != nullptr) { | |
| PublishedObjectMetadata metadata; | |
| metadata.location = Location(message.group_id, message.object_id); | |
| metadata.subgroup = std::nullopt; | |
| metadata.properties = message.properties; | |
| metadata.status = message.object_status; | |
| metadata.publisher_priority = message.publisher_priority; | |
| metadata.payload_length = payload->size(); | |
| metadata.arrival_time = callbacks_.clock->Now(); | |
| visitor->OnObjectFragment(track->full_track_name(), metadata, *payload, | |
| /*offset=*/0); | |
| } | |
| } | |
| void MoqtSession::OnCanCreateNewOutgoingBidirectionalStream() { | |
| while (!pending_bidi_streams_.empty() && | |
| session_->CanOpenNextOutgoingBidirectionalStream()) { | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| pending_bidi_streams_.front()->BindStream(stream); | |
| // TODO(vasilvv): Distinguish between control and and non-control bidi | |
| // streams in trace_recorder_. | |
| trace_recorder_.RecordControlStreamCreated(stream->GetStreamId()); | |
| stream->SetVisitor(std::move(pending_bidi_streams_.front())); | |
| pending_bidi_streams_.pop_front(); | |
| stream->visitor()->OnCanWrite(); | |
| } | |
| } | |
| void MoqtSession::Error(MoqtError code, absl::string_view error) { | |
| if (!error_.empty() || is_closing_) { | |
| // Avoid erroring out twice. | |
| return; | |
| } | |
| QUICHE_DLOG(INFO) << ENDPOINT << "MOQT session closed with code: " | |
| << static_cast<int>(code) << " and message: " << error; | |
| error_ = std::string(error); | |
| session_->CloseSession(static_cast<uint64_t>(code), error); | |
| std::move(callbacks_.session_terminated_callback)(error); | |
| CleanUpState(); | |
| } | |
| std::unique_ptr<MoqtNamespaceTask> MoqtSession::SubscribeNamespace( | |
| TrackNamespace& prefix, const MessageParameters& parameters, | |
| MoqtResponseCallback response_callback) { | |
| if (received_goaway_ || sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to send SUBSCRIBE_NAMESPACE after GOAWAY"; | |
| return nullptr; | |
| } | |
| if (prefix.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to subscribe to a reserved track namespace"; | |
| return nullptr; | |
| } | |
| if (!outgoing_subscribe_namespace_.SubscribeNamespace(prefix)) { | |
| return nullptr; | |
| } | |
| std::unique_ptr<MoqtSubscribeNamespaceRequestStream> state = | |
| std::make_unique<MoqtSubscribeNamespaceRequestStream>( | |
| &framer_, ControlMessageParser(), next_request_id_, | |
| [weakptr = GetWeakPtr()](const TrackNamespace& prefix) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->outgoing_subscribe_namespace_.UnsubscribeNamespace( | |
| prefix); | |
| } | |
| }, | |
| [weakptr = GetWeakPtr()](MoqtError error, absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(error, reason); | |
| } | |
| }, | |
| std::move(response_callback)); | |
| MoqtSubscribeNamespaceRequestStream* state_ptr = state.get(); | |
| if (session_->CanOpenNextOutgoingBidirectionalStream()) { | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| state->BindStream(stream); | |
| stream->SetVisitor(std::move(state)); | |
| } else { | |
| pending_bidi_streams_.push_back(std::move(state)); | |
| } | |
| MoqtSubscribeNamespace message; | |
| message.request_id = NextRequestId(); | |
| message.track_namespace_prefix = prefix; | |
| message.parameters = parameters; | |
| SanitizeParameters(message.parameters, MoqtMessageType::kSubscribeNamespace); | |
| state_ptr->SendOrBufferMessageOrFatal( | |
| framer_.SerializeSubscribeNamespace(message)); | |
| QUIC_DLOG(INFO) << ENDPOINT << "Sent SUBSCRIBE_NAMESPACE message for " | |
| << message.track_namespace_prefix; | |
| return state_ptr->CreateTask(prefix); | |
| } | |
| bool MoqtSession::SubscribeTracks(TrackNamespace& prefix, | |
| const MessageParameters& parameters, | |
| MoqtResponseCallback response_callback) { | |
| if (prefix.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to subscribe to a reserved track namespace"; | |
| return false; | |
| } | |
| return false; | |
| } | |
| void MoqtSession::UnsubscribeTracks(TrackNamespace& prefix) { | |
| // Do nothing. | |
| } | |
| bool MoqtSession::TrackStatus(const FullTrackName& name, | |
| const MessageParameters& parameters, | |
| TrackStatusResponseCallback response_callback) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| if (received_goaway_ || sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send TRACK_STATUS after GOAWAY"; | |
| return false; | |
| } | |
| if (name.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to get status of a reserved track name"; | |
| return false; | |
| } | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| if (stream == nullptr) { | |
| return false; | |
| } | |
| MessageParameters sanitized_parameters = parameters; | |
| SanitizeParameters(sanitized_parameters, MoqtMessageType::kTrackStatus); | |
| uint64_t request_id = NextRequestId(); | |
| auto stream_visitor = std::make_unique<MoqtTrackStatusRequestStream>( | |
| &framer_, ControlMessageParser(), request_id, name, sanitized_parameters, | |
| [session_weak = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_weak); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| std::move(response_callback)); | |
| MoqtTrackStatusRequestStream* stream_visitor_ptr = stream_visitor.get(); | |
| stream->SetVisitor(std::move(stream_visitor)); | |
| stream_visitor_ptr->BindStream(stream); | |
| return true; | |
| } | |
| bool MoqtSession::PublishNamespace( | |
| const TrackNamespace& track_namespace, const MessageParameters& parameters, | |
| MoqtResponseCallback response_callback, | |
| quiche::SingleUseCallback<void()> cancel_callback) { | |
| if (is_closing_ || publish_namespace_requests_.contains(track_namespace)) { | |
| return false; | |
| } | |
| if (received_goaway_ || sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to send PUBLISH_NAMESPACE after GOAWAY"; | |
| return false; | |
| } | |
| if (track_namespace.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to publish a reserved namespace"; | |
| return false; | |
| } | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| if (stream == nullptr) { | |
| return false; | |
| } | |
| MessageParameters sanitized_parameters = parameters; | |
| SanitizeParameters(sanitized_parameters, MoqtMessageType::kPublishNamespace); | |
| auto stream_visitor = std::make_unique<MoqtPublishNamespaceRequestStream>( | |
| track_namespace, sanitized_parameters, &framer_, ControlMessageParser(), | |
| NextRequestId(), | |
| [weakptr = GetWeakPtr(), callback = std::move(cancel_callback)]( | |
| const TrackNamespace& prefix) mutable { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->publish_namespace_requests_.erase(prefix); | |
| std::move(callback)(); | |
| }, | |
| [weakptr = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->Error(code, reason); | |
| }, | |
| std::move(response_callback)); | |
| MoqtPublishNamespaceRequestStream* stream_ptr = stream_visitor.get(); | |
| publish_namespace_requests_.emplace(track_namespace, stream_ptr); | |
| stream->SetVisitor(std::move(stream_visitor)); | |
| stream_ptr->BindStream(stream); | |
| return true; | |
| } | |
| bool MoqtSession::PublishNamespaceUpdate( | |
| const TrackNamespace& track_namespace, MessageParameters& parameters, | |
| MoqtResponseCallback response_callback) { | |
| if (is_closing_) { | |
| return false; | |
| } | |
| auto it = publish_namespace_requests_.find(track_namespace); | |
| if (it == publish_namespace_requests_.end()) { | |
| QUICHE_BUG(quic_bug_publish_namespace_update_after_closure) | |
| << "Tried to send PUBLISH_NAMESPACE_UPDATE for unknown namespace " | |
| << track_namespace; | |
| return false; | |
| } | |
| it->second->CheckStatus(it->second->SendRequestUpdate( | |
| NextRequestId(), parameters, std::move(response_callback), | |
| MoqtMessageType::kPublishNamespace)); | |
| return true; | |
| } | |
| bool MoqtSession::PublishNamespaceDone(const TrackNamespace& track_namespace) { | |
| if (is_closing_) { | |
| return false; | |
| } | |
| auto it = publish_namespace_requests_.find(track_namespace); | |
| if (it == publish_namespace_requests_.end()) { | |
| QUICHE_BUG(quic_bug_publish_namespace_update_after_closure) | |
| << "Tried to reset PUBLISH_NAMESPACE for unknown namespace " | |
| << track_namespace; | |
| return false; | |
| } | |
| it->second->Reset(kResetCodeCancelled); | |
| QUIC_DLOG(INFO) << ENDPOINT << "Revoked PUBLISH_NAMESPACE message for " | |
| << track_namespace; | |
| return true; | |
| } | |
| bool MoqtSession::PublishNamespaceCancel( | |
| const TrackNamespace& track_namespace, | |
| webtransport::StreamErrorCode error_code) { | |
| auto it = publish_namespace_responses_.find(track_namespace); | |
| if (it == publish_namespace_responses_.end()) { | |
| QUICHE_BUG(quic_bug_publish_namespace_update_after_closure) | |
| << "Tried to reset PUBLISH_NAMESPACE for unknown namespace " | |
| << track_namespace; | |
| return false; | |
| } | |
| it->second->Reset(error_code); | |
| QUIC_DLOG(INFO) << ENDPOINT << "Signalled disinterest in PUBLISH_NAMESPACE " | |
| << " for " << track_namespace; | |
| return true; | |
| } | |
| bool MoqtSession::Subscribe(const FullTrackName& name, | |
| SubscribeVisitor* absl_nonnull visitor, | |
| const MessageParameters& parameters) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| if (subscribe_by_name_.contains(name)) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send SUBSCRIBE for track " << name | |
| << " which is already subscribed"; | |
| return false; | |
| } | |
| if (received_goaway_ || sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send SUBSCRIBE after GOAWAY"; | |
| return false; | |
| } | |
| if (!session_->CanOpenNextOutgoingBidirectionalStream()) { | |
| return false; // Do not retry opening a SUBSCRIBE stream. | |
| } | |
| if (name.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to subscribe to a reserved track name"; | |
| return false; | |
| } | |
| MessageParameters sanitized_parameters = parameters; | |
| SanitizeParameters(sanitized_parameters, MoqtMessageType::kSubscribe); | |
| auto stream_visitor = std::make_unique<MoqtSubscribeRequestStream>( | |
| &framer_, ControlMessageParser(), NextRequestId(), | |
| [weak_session = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSessionInterface* session = weak_session.GetIfAvailable(); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->Error(code, reason); | |
| }, | |
| name, visitor, sanitized_parameters, | |
| [weakptr = GetWeakPtr()](LiveSubscriber* track) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr || !track->track_alias().has_value()) { | |
| return false; | |
| } | |
| auto [it, success] = session->subscribe_by_alias_.try_emplace( | |
| *track->track_alias(), track); | |
| return success; | |
| }, | |
| [weakptr = GetWeakPtr()](LiveSubscriber* track) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->subscribe_by_name_.erase(track->full_track_name()); | |
| if (track->track_alias().has_value()) { | |
| session->subscribe_by_alias_.erase(*track->track_alias()); | |
| } | |
| }, | |
| callbacks_.clock, alarm_factory_.get()); | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| QUICHE_CHECK(stream != nullptr); | |
| MoqtSubscribeRequestStream* stream_visitor_ptr = stream_visitor.get(); | |
| stream->SetVisitor(std::move(stream_visitor)); | |
| stream_visitor_ptr->BindStream(stream); | |
| subscribe_by_name_[name] = stream_visitor_ptr->track(); | |
| if (SupportsObjectAck()) { | |
| visitor->OnCanAckObjects( | |
| [weak_this = GetWeakPtr(), name](uint64_t group, uint64_t object, | |
| quic::QuicTimeDelta time_delta) { | |
| if (MoqtSession* session = MoqtSessionFromWeakPtr(weak_this); | |
| session != nullptr) { | |
| session->SendObjectAck(name, group, object, time_delta); | |
| } | |
| }); | |
| } | |
| return true; | |
| } | |
| bool MoqtSession::SubscribeUpdate(const FullTrackName& name, | |
| const MessageParameters& parameters, | |
| MoqtResponseCallback response_callback) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| auto it = subscribe_by_name_.find(name); | |
| if (it == subscribe_by_name_.end()) { | |
| return false; | |
| } | |
| MessageParameters update_parameters = parameters; | |
| if (parameters.new_group_request.has_value() && | |
| !it->second->dynamic_groups() && !it->second->ErrorIsAllowed()) { | |
| // Sending NEW_GROUP_REQUEST if the Track Properties have arrived and do not | |
| // include DYNAMIC_GROUPS. | |
| update_parameters.new_group_request.reset(); | |
| } | |
| return it->second->request_stream() | |
| ->SendRequestUpdate(NextRequestId(), update_parameters, | |
| std::move(response_callback), | |
| MoqtMessageType::kSubscribe) | |
| .ok(); | |
| } | |
| bool MoqtSession::PublishUpdate(const FullTrackName& name, | |
| const MessageParameters& parameters, | |
| MoqtResponseCallback response_callback) { | |
| // TODO(martinduke): Implement this. | |
| return false; | |
| } | |
| void MoqtSession::Unsubscribe(const FullTrackName& name) { | |
| if (is_closing_) { | |
| return; | |
| } | |
| QUICHE_DCHECK(name.IsValid()); | |
| auto it = subscribe_by_name_.find(name); | |
| if (it == subscribe_by_name_.end()) { | |
| return; | |
| } | |
| it->second->request_stream()->Reset(kResetCodeCancelled); | |
| } | |
| bool MoqtSession::Publish( | |
| std::shared_ptr<MoqtTrackPublisher> absl_nonnull publisher, | |
| const MessageParameters& parameters, const TrackProperties& properties, | |
| MoqtResponseCallback response_callback) { | |
| if (received_goaway_ || sent_goaway_) { | |
| QUICHE_DLOG(INFO) << ENDPOINT << "Tried to send PUBLISH after GOAWAY"; | |
| return false; | |
| } | |
| const FullTrackName& name = publisher->GetTrackName(); | |
| QUICHE_DCHECK(name.IsValid()); | |
| if (!session_->CanOpenNextOutgoingBidirectionalStream()) { | |
| return false; // Do not retry opening a PUBLISH stream. | |
| } | |
| if (name.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to publish a reserved track name"; | |
| return false; | |
| } | |
| auto it = subscribed_track_names_.find(name); | |
| if (it != subscribed_track_names_.end()) { | |
| if (it->second->established()) { | |
| QUICHE_DLOG(INFO) << ENDPOINT << "Tried to send PUBLISH for track " | |
| << name << " which is already published"; | |
| return false; | |
| } | |
| it->second->OnSubscribeRejected( | |
| MoqtRequestErrorInfo{RequestErrorCode::kDuplicateSubscription, | |
| std::nullopt, "PUBLISH is coming"}); | |
| } | |
| auto stream_visitor = std::make_unique<MoqtPublishRequestStream>( | |
| &framer_, ControlMessageParser(), | |
| [weak_session = GetWeakPtr()](LivePublisher* publisher) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->subscribed_track_names_.erase( | |
| publisher->publisher().GetTrackName()); | |
| session->published_subscriptions_.erase(publisher->request_id()); | |
| }, | |
| [weak_session = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSessionInterface* session = weak_session.GetIfAvailable(); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->Error(code, reason); | |
| }, | |
| [weak_session = GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| std::move(response_callback)); | |
| MessageParameters sanitized_parameters = parameters; | |
| SanitizeParameters(sanitized_parameters, MoqtMessageType::kPublish); | |
| auto publish_state = std::make_unique<LivePublisher>( | |
| framer_, publisher, stream_visitor.get(), next_request_id_, | |
| next_local_track_alias_, sanitized_parameters, | |
| weak_ptr_factory_for_publishers_.Create(), true); | |
| LivePublisher* publisher_ptr = publish_state.get(); | |
| stream_visitor->SetPublisher(std::move(publish_state)); | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| MoqtPublishRequestStream* stream_visitor_ptr = stream_visitor.get(); | |
| stream->SetVisitor(std::move(stream_visitor)); | |
| stream_visitor_ptr->BindStream(stream); | |
| next_request_id_ += 2; | |
| ++next_local_track_alias_; | |
| publisher->AddObjectListener(publisher_ptr, sanitized_parameters); | |
| return true; | |
| } | |
| std::unique_ptr<MoqtFetchTask> MoqtSession::Fetch( | |
| const FullTrackName& name, FetchResponseCallback callback, Location start, | |
| uint64_t end_group, std::optional<uint64_t> end_object, | |
| const MessageParameters& parameters) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| if (received_goaway_ || sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send FETCH after GOAWAY"; | |
| return nullptr; | |
| } | |
| if (name.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to fetch a reserved track name"; | |
| return nullptr; | |
| } | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| if (stream == nullptr) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send FETCH but no more streams"; | |
| return nullptr; | |
| } | |
| MessageParameters sanitized_parameters = parameters; | |
| SanitizeParameters(sanitized_parameters, MoqtMessageType::kFetch); | |
| uint64_t request_id = NextRequestId(); | |
| auto task = std::make_unique<UpstreamFetchTask>(); | |
| auto fetch = std::make_unique<MoqtFetchRequestStream>( | |
| &framer_, ControlMessageParser(), request_id, name, start, | |
| Location(end_group, end_object.value_or(kMaxObjectId)), | |
| sanitized_parameters, task.get(), | |
| [weak_session = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->Error(code, reason); | |
| }, | |
| std::move(callback), | |
| [weak_session = GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->fetch_by_id_.erase(request_id); | |
| }); | |
| MoqtFetchRequestStream* fetch_visitor = fetch.get(); | |
| fetch_by_id_.emplace(request_id, fetch_visitor); | |
| stream->SetVisitor(std::move(fetch)); | |
| fetch_visitor->BindStream(stream); | |
| return task; | |
| } | |
| bool MoqtSession::RelativeJoiningFetch(const FullTrackName& name, | |
| SubscribeVisitor* visitor, | |
| uint64_t num_previous_groups, | |
| const MessageParameters& parameters) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| if (name.DoesNotExist()) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to fetch a reserved track name"; | |
| return false; | |
| } | |
| std::unique_ptr<MoqtFetchTask> fetch_task = RelativeJoiningFetch( | |
| name, visitor, [](std::variant<FetchOkData, MoqtRequestErrorInfo>) {}, | |
| num_previous_groups, parameters); | |
| if (fetch_task == nullptr) { | |
| return false; | |
| } | |
| LiveSubscriber* subscribe = SubscribeByName(name); | |
| if (subscribe == nullptr || subscribe->is_fetch()) { | |
| // fetch_task will be released on exit. | |
| return false; | |
| } | |
| subscribe->OnJoiningFetchReady(std::move(fetch_task)); | |
| return true; | |
| } | |
| std::unique_ptr<MoqtFetchTask> MoqtSession::RelativeJoiningFetch( | |
| const FullTrackName& name, SubscribeVisitor* visitor, | |
| FetchResponseCallback callback, uint64_t num_previous_groups, | |
| const MessageParameters& parameters) { | |
| QUICHE_DCHECK(name.IsValid()); | |
| MessageParameters subscribe_parameters = parameters; | |
| subscribe_parameters.subscription_filter.emplace( | |
| MoqtFilterType::kLargestObject); | |
| uint64_t subscribe_request_id = next_request_id_; | |
| if (!Subscribe(name, visitor, subscribe_parameters)) { | |
| return nullptr; | |
| } | |
| webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream(); | |
| if (stream == nullptr) { | |
| // TODO(martinduke): This is a spot where the bool return value is not all | |
| // that helpful, but the problem will go away when the whole transaction | |
| // occurs on one stream. | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Tried to send JOINING FETCH but no more " | |
| "streams"; | |
| return nullptr; | |
| } | |
| QUIC_DLOG(INFO) << ENDPOINT << "Sent Joining FETCH message for " << name; | |
| MessageParameters fetch_parameters = parameters; | |
| SanitizeParameters(fetch_parameters, MoqtMessageType::kFetch); | |
| uint64_t request_id = NextRequestId(); | |
| auto task = std::make_unique<UpstreamFetchTask>(); | |
| auto fetch = std::make_unique<MoqtFetchRequestStream>( | |
| &framer_, ControlMessageParser(), request_id, name, subscribe_request_id, | |
| num_previous_groups, /*relative=*/true, fetch_parameters, task.get(), | |
| [weak_session = GetWeakPtr()](MoqtError code, absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->Error(code, reason); | |
| }, | |
| std::move(callback), | |
| [weak_session = GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weak_session); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->fetch_by_id_.erase(request_id); | |
| }); | |
| MoqtFetchRequestStream* fetch_visitor = fetch.get(); | |
| fetch_by_id_.emplace(request_id, fetch_visitor); | |
| stream->SetVisitor(std::move(fetch)); | |
| fetch_visitor->BindStream(stream); | |
| return task; | |
| } | |
| void MoqtSession::GoAway(absl::string_view new_session_uri) { | |
| if (sent_goaway_) { | |
| QUIC_DLOG(INFO) << ENDPOINT << "Tried to send multiple GOAWAY"; | |
| return; | |
| } | |
| if (!new_session_uri.empty() && | |
| parameters_.perspective == Perspective::IS_CLIENT) { | |
| QUIC_DLOG(INFO) << ENDPOINT | |
| << "Client tried to send GOAWAY with new session URI"; | |
| return; | |
| } | |
| MoqtGoAway message; | |
| message.new_session_uri = std::string(new_session_uri); | |
| message.timeout = kDefaultGoAwayTimeout; | |
| message.request_id = next_incoming_request_id_; | |
| SendControlMessage(framer_.SerializeGoAway(message)); | |
| sent_goaway_ = true; | |
| goaway_timeout_alarm_ = absl::WrapUnique( | |
| alarm_factory_->CreateAlarm(new GoAwayTimeoutDelegate(this))); | |
| goaway_timeout_alarm_->Set(callbacks_.clock->ApproximateNow() + | |
| kDefaultGoAwayTimeout); | |
| } | |
| void MoqtSession::GoAwayTimeoutDelegate::OnAlarm() { | |
| session_->Error(MoqtError::kGoawayTimeout, | |
| "Peer did not close session after GOAWAY"); | |
| } | |
| void MoqtSession::UpdateTrackPriority( | |
| const FullTrackName& name, std::optional<MoqtTrackPriority> old_priority, | |
| MoqtTrackPriority new_priority) { | |
| if (old_priority.has_value()) { | |
| auto [start, end] = | |
| requests_with_queued_streams_.equal_range(*old_priority); | |
| for (auto it = start; it != end; ++it) { | |
| if (std::holds_alternative<FullTrackName>(it->second) && | |
| std::get<FullTrackName>(it->second) == name) { | |
| requests_with_queued_streams_.erase(it); | |
| break; | |
| } | |
| } | |
| } | |
| requests_with_queued_streams_.emplace(new_priority, name); | |
| } | |
| void MoqtSession::UpdateTrackPriority( | |
| webtransport::StreamId stream_id, | |
| std::optional<MoqtTrackPriority> old_priority, | |
| MoqtTrackPriority new_priority) { | |
| if (old_priority.has_value()) { | |
| auto [start, end] = | |
| requests_with_queued_streams_.equal_range(*old_priority); | |
| for (auto it = start; it != end; ++it) { | |
| if (std::holds_alternative<webtransport::StreamId>(it->second) && | |
| std::get<webtransport::StreamId>(it->second) == stream_id) { | |
| requests_with_queued_streams_.erase(it); | |
| break; | |
| } | |
| } | |
| } | |
| requests_with_queued_streams_.emplace(new_priority, stream_id); | |
| } | |
| std::shared_ptr<MoqtTrackPublisher> MoqtSession::GetTrackPublisher( | |
| const FullTrackName& name) { | |
| if (publisher_ == nullptr) { | |
| return nullptr; | |
| } | |
| return publisher_->GetTrack(name); | |
| } | |
| MoqtPublishingMonitorInterface* MoqtSession::ReleaseMonitoringInterface( | |
| const FullTrackName& name) { | |
| auto it = monitoring_interfaces_for_published_tracks_.find(name); | |
| if (it == monitoring_interfaces_for_published_tracks_.end()) { | |
| return nullptr; | |
| } | |
| MoqtPublishingMonitorInterface* interface = it->second; | |
| monitoring_interfaces_for_published_tracks_.erase(it); | |
| return interface; | |
| } | |
| LiveSubscriber* MoqtSession::SubscribeByAlias(uint64_t track_alias) { | |
| auto it = subscribe_by_alias_.find(track_alias); | |
| if (it == subscribe_by_alias_.end()) { | |
| return nullptr; | |
| } | |
| return it->second; | |
| } | |
| LiveSubscriber* MoqtSession::SubscribeByName(const FullTrackName& track_name) { | |
| auto it = subscribe_by_name_.find(track_name); | |
| if (it == subscribe_by_name_.end()) { | |
| return nullptr; | |
| } | |
| return it->second; | |
| } | |
| MoqtFetchRequestStream* MoqtSession::FetchById(uint64_t request_id) { | |
| auto it = fetch_by_id_.find(request_id); | |
| if (it == fetch_by_id_.end()) { | |
| return nullptr; | |
| } | |
| return it->second; | |
| } | |
| void MoqtSession::OnCanCreateNewOutgoingUnidirectionalStream() { | |
| while (!requests_with_queued_streams_.empty() && | |
| session_->CanOpenNextOutgoingUnidirectionalStream()) { | |
| auto next = requests_with_queued_streams_.begin(); | |
| if (std::holds_alternative<FullTrackName>(next->second)) { | |
| auto it = | |
| subscribed_track_names_.find(std::get<FullTrackName>(next->second)); | |
| requests_with_queued_streams_.erase(next); | |
| if (it != subscribed_track_names_.end()) { | |
| it->second->OnCanCreateNewUniStream(); | |
| } | |
| continue; | |
| } | |
| // FETCH. | |
| webtransport::StreamId stream_id = | |
| std::get<webtransport::StreamId>(next->second); | |
| requests_with_queued_streams_.erase(next); | |
| webtransport::Stream* stream = session_->GetStreamById(stream_id); | |
| if (stream == nullptr) { | |
| // The request is gone, so remove it from the queue and continue. | |
| continue; | |
| } | |
| auto fetch = absl::down_cast<MoqtFetchResponseStream*>(stream->visitor()); | |
| if (fetch == nullptr) { | |
| QUICHE_BUG(queued_uni_stream_invalid_request_type) | |
| << "Unknown stream type for request " << stream_id; | |
| continue; | |
| } | |
| fetch->OnDataStreamOpen(session_->OpenOutgoingUnidirectionalStream(), | |
| &trace_recorder_); | |
| } | |
| } | |
| absl::Status MoqtSession::ValidateNewIncomingRequestId(uint64_t request_id) { | |
| if ((request_id % 2 == 0) != | |
| (parameters_.perspective == Perspective::IS_SERVER)) { | |
| QUIC_DLOG(INFO) << parameters_.perspective | |
| << "Request ID evenness incorrect"; | |
| Error(MoqtError::kInvalidRequestId, "Request ID evenness incorrect"); | |
| return absl::InvalidArgumentError("Request ID evenness incorrect"); | |
| } | |
| // Check the one request ID map we have for the value. Most active request IDs | |
| // are not tracked at the session level. | |
| if (published_subscriptions_.contains(request_id)) { | |
| QUIC_DLOG(INFO) << parameters_.perspective << "Duplicate Request ID"; | |
| Error(MoqtError::kInvalidRequestId, "Duplicate Request ID"); | |
| return absl::InvalidArgumentError("Duplicate Request ID"); | |
| } | |
| next_incoming_request_id_ = request_id + 2; | |
| return absl::OkStatus(); | |
| } | |
| void MoqtSession::UnknownBidiStream::OnCanRead() { | |
| absl::StatusOr<uint64_t> type = parser_.ReadStreamType(); | |
| if (absl::IsUnavailable(type.status())) { | |
| return; | |
| } | |
| if (absl::IsInvalidArgument(type.status())) { | |
| // Received a FIN before any type has been available, which is malformed. | |
| session_->Error(MoqtError::kProtocolViolation, type.status().message()); | |
| return; | |
| } | |
| if (!type.ok()) { | |
| // The result is neither of "OK", "no type available", or "parse error". | |
| // This is unexpected; treat it as an internal error, and reset the stream. | |
| stream_->SendStopSending(kResetCodeInternalError); | |
| stream_->ResetWithUserCode(kResetCodeInternalError); | |
| return; | |
| } | |
| MoqtMessageType message_type = static_cast<MoqtMessageType>(*type); | |
| switch (message_type) { | |
| case MoqtMessageType::kSubscribeNamespace: { | |
| auto namespace_stream = | |
| std::make_unique<MoqtSubscribeNamespaceResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| [weakptr = session_->GetWeakPtr()](const TrackNamespace& prefix) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| return session->incoming_subscribe_namespace_ | |
| .SubscribeNamespace(prefix); | |
| } | |
| return true; | |
| }, | |
| [weakptr = session_->GetWeakPtr()](const TrackNamespace& prefix) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->incoming_subscribe_namespace_.UnsubscribeNamespace( | |
| prefix); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| session_->callbacks_.incoming_subscribe_namespace_callback); | |
| namespace_stream->BindStream(std::move(parser_)); | |
| MoqtSubscribeNamespaceResponseStream* temp_stream = | |
| namespace_stream.get(); | |
| stream_->SetVisitor(std::move(namespace_stream)); | |
| // The UnknownBidiStream object is deleted; no class access after this | |
| // point. | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| case MoqtMessageType::kSubscribeTracks: { | |
| // TODO(martinduke): Implement this. | |
| MoqtControlMessageQueue queue(stream_); | |
| if (!queue | |
| .SendOrBufferMessage( | |
| session_->framer_.SerializeRequestError(MoqtRequestError{ | |
| RequestErrorCode::kNotSupported, std::nullopt, | |
| "SUBSCRIBE_TRACKS is not supported"})) | |
| .ok()) { | |
| session_->Error(MoqtError::kInternalError, "Internal write error"); | |
| return; | |
| } | |
| break; | |
| } | |
| case MoqtMessageType::kPublishNamespace: { | |
| auto publish_namespace_stream = | |
| std::make_unique<MoqtPublishNamespaceResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| [weakptr = session_->GetWeakPtr()](const TrackNamespace& prefix, | |
| MoqtBidiStreamBase* stream) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return false; | |
| } | |
| auto [it, success] = | |
| session->publish_namespace_responses_.try_emplace(prefix, | |
| stream); | |
| return success; | |
| }, | |
| [weakptr = session_->GetWeakPtr()](const TrackNamespace& prefix) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->publish_namespace_responses_.erase(prefix); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| [weakptr = session_->GetWeakPtr()]( | |
| const TrackNamespace& track_namespace, | |
| const MessageParameters* absl_nullable parameters, | |
| MoqtResponseCallback callback) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->callbacks_.incoming_publish_namespace_callback( | |
| track_namespace, parameters, std::move(callback)); | |
| }); | |
| publish_namespace_stream->BindStream(std::move(parser_)); | |
| MoqtPublishNamespaceResponseStream* temp_stream = | |
| publish_namespace_stream.get(); | |
| stream_->SetVisitor(std::move(publish_namespace_stream)); | |
| // The UnknownBidiStream object is deleted; no class access after this | |
| // point. | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| case MoqtMessageType::kPublish: { | |
| auto publish_stream = std::make_unique<MoqtPublishResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| session_->callbacks_.clock, session_->alarm_factory(), | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| &session_->callbacks_.incoming_publish_callback, | |
| [weakptr = session_->GetWeakPtr()](LiveSubscriber* track) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return false; | |
| } | |
| QUICHE_BUG_IF(quiche_bug_publish_no_track_alias, | |
| !track->track_alias().has_value()) | |
| << "PUBLISH with no track alias"; | |
| if (!track->track_alias().has_value()) { | |
| return false; | |
| } | |
| auto [alias_it, alias_inserted] = | |
| session->subscribe_by_alias_.try_emplace(*track->track_alias(), | |
| track); | |
| if (!alias_inserted) { | |
| // Already a PUBLISH or an established SUBSCRIBE. | |
| return false; | |
| } | |
| auto it = | |
| session->subscribe_by_name_.find(track->full_track_name()); | |
| if (it != session->subscribe_by_name_.end()) { | |
| // It's a pending SUBSCRIBE; kill it, but use the parameters and | |
| // visitor from the SUBSCRIBE. | |
| track->Update(it->second->const_parameters()); | |
| track->set_visitor(it->second->ReleaseVisitor()); | |
| session->Unsubscribe(it->second->full_track_name()); | |
| } | |
| auto [name_it, name_inserted] = | |
| session->subscribe_by_name_.try_emplace( | |
| track->full_track_name(), track); | |
| QUICHE_DCHECK(name_inserted); | |
| return true; | |
| }, | |
| [weakptr = session_->GetWeakPtr()](LiveSubscriber* track) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->subscribe_by_name_.erase(track->full_track_name()); | |
| if (track->track_alias().has_value()) { | |
| session->subscribe_by_alias_.erase(*track->track_alias()); | |
| } | |
| } | |
| }); | |
| publish_stream->BindStream(std::move(parser_)); | |
| MoqtPublishResponseStream* temp_stream = publish_stream.get(); | |
| stream_->SetVisitor(std::move(publish_stream)); | |
| // The UnknownBidiStream object is deleted; no class access after this | |
| // point. | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| case MoqtMessageType::kSubscribe: { | |
| auto subscribe_stream = std::make_unique<MoqtSubscribeResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| session_->next_local_track_alias_++, | |
| [weakptr = session_->GetWeakPtr()](LivePublisher* subscription) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return true; | |
| } | |
| auto [it, success] = session->published_subscriptions_.try_emplace( | |
| subscription->request_id(), subscription); | |
| if (!success) { | |
| return false; | |
| } | |
| auto [it2, success2] = session->subscribed_track_names_.try_emplace( | |
| subscription->publisher().GetTrackName(), subscription); | |
| return success2; | |
| }, | |
| [weakptr = session_->GetWeakPtr()](LivePublisher* subscription) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| session->published_subscriptions_.erase(subscription->request_id()); | |
| session->subscribed_track_names_.erase( | |
| subscription->publisher().GetTrackName()); | |
| }, | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| session_->weak_ptr_factory_for_publishers_.Create()); | |
| subscribe_stream->BindStream(std::move(parser_)); | |
| MoqtSubscribeResponseStream* temp_stream = subscribe_stream.get(); | |
| stream_->SetVisitor(std::move(subscribe_stream)); | |
| // The UnknownBidiStream object is deleted; no class access after this | |
| // point. | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| case MoqtMessageType::kTrackStatus: { | |
| auto track_status_stream = | |
| std::make_unique<MoqtTrackStatusResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| session_->weak_ptr_factory_for_publishers_.Create()); | |
| track_status_stream->BindStream(std::move(parser_)); | |
| MoqtTrackStatusResponseStream* temp_stream = track_status_stream.get(); | |
| stream_->SetVisitor(std::move(track_status_stream)); | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| case MoqtMessageType::kFetch: { | |
| auto fetch_stream = std::make_unique<MoqtFetchResponseStream>( | |
| &session_->framer_, session_->ControlMessageParser(), | |
| session_->publisher_, | |
| [weakptr = session_->GetWeakPtr()](MoqtError code, | |
| absl::string_view reason) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session != nullptr) { | |
| session->Error(code, reason); | |
| } | |
| }, | |
| [weakptr = session_->GetWeakPtr()](uint64_t request_id) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return absl::NotFoundError("Session is gone"); | |
| } | |
| return session->ValidateNewIncomingRequestId(request_id); | |
| }, | |
| // OpenStreamCallback | |
| [weakptr = session_->GetWeakPtr()](webtransport::StreamId stream_id, | |
| MoqtTrackPriority priority) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| if (!session->session_->CanOpenNextOutgoingUnidirectionalStream()) { | |
| session->UpdateTrackPriority(stream_id, std::nullopt, priority); | |
| return; | |
| } | |
| webtransport::Stream* wt_stream = | |
| session->session_->GetStreamById(stream_id); | |
| if (wt_stream == nullptr) { | |
| QUICHE_BUG( | |
| quiche_bug_OpenStreamCallback_called_by_nonexistent_stream) | |
| << "OpenStreamCallback called by non-existent stream " | |
| << stream_id; | |
| return; | |
| } | |
| MoqtFetchResponseStream* response_stream = | |
| absl::down_cast<MoqtFetchResponseStream*>(wt_stream->visitor()); | |
| if (response_stream == nullptr) { | |
| QUICHE_BUG(quiche_bug_fetch_response_stream_not_found) | |
| << "Failed to get fetch response stream for id " << stream_id; | |
| return; | |
| } | |
| response_stream->OnDataStreamOpen( | |
| session->session_->OpenOutgoingUnidirectionalStream(), | |
| &session->trace_recorder()); | |
| }, | |
| // GetSubscriptionCallback | |
| [weakptr = | |
| session_->GetWeakPtr()](uint64_t request_id) -> LivePublisher* { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(weakptr); | |
| if (session == nullptr) { | |
| return nullptr; | |
| } | |
| auto it = session->published_subscriptions_.find(request_id); | |
| if (it == session->published_subscriptions_.end()) { | |
| return nullptr; | |
| } | |
| return it->second; | |
| }); | |
| fetch_stream->BindStream(std::move(parser_)); | |
| MoqtFetchResponseStream* temp_stream = fetch_stream.get(); | |
| stream_->SetVisitor(std::move(fetch_stream)); | |
| temp_stream->OnCanRead(); | |
| break; | |
| } | |
| default: | |
| session_->Error(MoqtError::kProtocolViolation, | |
| "Unexpected message type received to start bidi stream"); | |
| return; | |
| } | |
| } | |
| void MoqtSession::UnknownUniStream::OnCanRead() { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_); | |
| if (session == nullptr || session->is_closing_) { | |
| return; | |
| } | |
| absl::StatusOr<uint64_t> type = parser_.ReadStreamType(); | |
| if (absl::IsUnavailable(type.status())) { | |
| return; | |
| } | |
| if (absl::IsInvalidArgument(type.status())) { | |
| // Received a FIN before any type has been available, which is malformed. | |
| session->Error(MoqtError::kProtocolViolation, type.status().message()); | |
| return; | |
| } | |
| if (!type.ok()) { | |
| stream_->SendStopSending(kResetCodeInternalError); | |
| return; | |
| } | |
| if (*type == static_cast<uint64_t>(MoqtMessageType::kSetup)) { | |
| if (session->incoming_control_stream_.GetIfAvailable() != nullptr) { | |
| session->Error(MoqtError::kProtocolViolation, "Multiple control streams"); | |
| return; | |
| } | |
| session->trace_recorder().RecordControlStreamCreated( | |
| stream_->GetStreamId()); | |
| auto control_stream = | |
| std::make_unique<IncomingControlStream>(session, std::move(parser_)); | |
| IncomingControlStream* temp_stream = control_stream.get(); | |
| session->incoming_control_stream_ = temp_stream->GetWeakPtr(); | |
| // The line below destroys `this`. | |
| stream_->SetVisitor(std::move(control_stream)); | |
| temp_stream->OnCanRead(); | |
| return; | |
| } | |
| if (*type == kPaddingStreamType) { | |
| auto padding_stream = std::make_unique<IncomingPaddingStream>(stream_); | |
| IncomingPaddingStream* temp_stream = padding_stream.get(); | |
| // The line below destroys `this`. | |
| stream_->SetVisitor(std::move(padding_stream)); | |
| temp_stream->OnCanRead(); | |
| return; | |
| } | |
| if (!MoqtDataStreamType::FromValue(*type).has_value()) { | |
| session->Error(MoqtError::kProtocolViolation, "Unknown stream type"); | |
| return; | |
| } | |
| auto data_stream = std::make_unique<IncomingDataStream>( | |
| std::move(parser_), session, session->callbacks_.clock); | |
| IncomingDataStream* temp_stream = data_stream.get(); | |
| // The line below destroys `this`. | |
| stream_->SetVisitor(std::move(data_stream)); | |
| temp_stream->OnCanRead(); | |
| } | |
| MoqtSession::IncomingControlStream::IncomingControlStream( | |
| MoqtSession* absl_nonnull session, MoqtStreamTypeParser type_parser) | |
| : session_(session->GetWeakPtr()), | |
| parser_(std::move(type_parser)), | |
| weak_ptr_factory_(this) {} | |
| void MoqtSession::IncomingControlStream::OnCanRead() { | |
| quiche::QuicheWeakPtr<IncomingControlStream> weak_this = | |
| weak_ptr_factory_.Create(); | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_); | |
| if (session == nullptr || session->is_closing_) { | |
| return; | |
| } | |
| while (true) { | |
| absl::StatusOr<MoqtRawControlMessage> message = parser_.ReadNextMessage(); | |
| if (absl::IsUnavailable(message.status())) { | |
| return; | |
| } | |
| if (!message.ok()) { | |
| std::optional<MoqtError> error_code = | |
| GetMoqtErrorForStatus(message.status()); | |
| session->Error(error_code.value_or(MoqtError::kProtocolViolation), | |
| message.status().message()); | |
| return; | |
| } | |
| absl::Status status = ControlMessageDispatcher::DispatchControlMessage( | |
| *session, session->ControlMessageParser(), *message, "control"); | |
| // `DispatchControlMessage` might have closed the session by itself, | |
| // resulting in the stream and/or the session object being deleted. | |
| if (!weak_this.IsValid() || !session_.IsValid() || session->is_closing_) { | |
| return; | |
| } | |
| if (!status.ok()) { | |
| std::optional<MoqtError> error_code = GetMoqtErrorForStatus(status); | |
| session->Error(error_code.value_or(MoqtError::kProtocolViolation), | |
| status.message()); | |
| return; | |
| } | |
| } | |
| } | |
| void MoqtSession::IncomingControlStream::OnResetStreamReceived( | |
| webtransport::StreamErrorCode /*error*/) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_); | |
| if (session != nullptr) { | |
| session->Error(MoqtError::kProtocolViolation, | |
| "Control stream reset received"); | |
| } | |
| } | |
| MoqtSession::OutgoingControlStream::OutgoingControlStream( | |
| MoqtSession* absl_nonnull session, | |
| webtransport::Stream* absl_nonnull stream) | |
| : session_(session->GetWeakPtr()), | |
| outgoing_message_queue_(stream), | |
| weak_ptr_factory_(this) { | |
| if (stream != nullptr) { | |
| stream->SetPriority(webtransport::StreamPriority{ | |
| /*send_group_id=*/kMoqtSendGroupId, | |
| /*send_order=*/kMoqtControlStreamSendOrder}); | |
| } | |
| } | |
| void MoqtSession::OutgoingControlStream::OnCanWrite() { | |
| CheckStatus(outgoing_message_queue_.OnCanWrite()); | |
| } | |
| void MoqtSession::OutgoingControlStream::OnStopSendingReceived( | |
| webtransport::StreamErrorCode /*error*/) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_); | |
| if (session != nullptr) { | |
| session->Error(MoqtError::kProtocolViolation, | |
| "Control stream stop sending received"); | |
| } | |
| } | |
| void MoqtSession::OutgoingControlStream::CheckStatus(absl::Status status) { | |
| MoqtSession* session = MoqtSessionFromWeakPtr(session_); | |
| if (session == nullptr) { | |
| return; | |
| } | |
| if (!status.ok() && !session->is_closing_) { | |
| std::optional<MoqtError> error_code = GetMoqtErrorForStatus(status); | |
| session->Error(error_code.value_or(MoqtError::kInternalError), | |
| status.message()); | |
| } | |
| } | |
| absl::Status MoqtSession::OnControlMessage(const MoqtSetup& message) { | |
| if (peer_setup_received_) { | |
| return absl::InvalidArgumentError("Duplicate SETUP message"); | |
| } | |
| peer_setup_received_ = true; | |
| peer_supports_object_ack_ = | |
| message.options.support_object_acks.value_or(kDefaultSupportObjectAcks); | |
| QUIC_DLOG(INFO) << ENDPOINT << "Received the SETUP message"; | |
| // TODO: handle path. | |
| if (callbacks_.session_established_callback != nullptr) { | |
| MoqtSessionEstablishedCallback callback = | |
| std::move(callbacks_.session_established_callback); | |
| callbacks_.session_established_callback = nullptr; | |
| std::move(callback)(); | |
| } | |
| // Drain streams that were potentially stalled due to a missing SETUP. | |
| OnIncomingBidirectionalStreamAvailable(); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status MoqtSession::OnControlMessage(const MoqtGoAway& message) { | |
| if (received_goaway_) { | |
| return absl::InvalidArgumentError( | |
| "Received multiple GOAWAY on control stream"); | |
| } | |
| if (!message.request_id.has_value()) { | |
| return absl::InvalidArgumentError("GOAWAY missing request ID"); | |
| } | |
| if ((*message.request_id & 0x1) != (next_request_id_ & 0x1)) { | |
| Error(MoqtError::kInvalidRequestId, | |
| "GOAWAY request ID has incorrect parity"); | |
| return absl::OkStatus(); | |
| } | |
| received_goaway_ = true; | |
| if (callbacks_.goaway_received_callback == nullptr) { | |
| // An HTTP/3 or WebTransport GOAWAY already invoked the callback. | |
| return absl::OkStatus(); | |
| } | |
| MoqtSessionGoAwayCallback callback = | |
| std::move(callbacks_.goaway_received_callback); | |
| callbacks_.goaway_received_callback = nullptr; | |
| std::move(callback)(message.new_session_uri, message.timeout); | |
| return absl::OkStatus(); | |
| } | |
| void MoqtSession::OnMalformedTrack(ObjectSubscriber* track) { | |
| if (track->is_fetch()) { | |
| QUICHE_BUG(quiche_bug_malformed_fetch_track) | |
| << "Malformed FETCH track should be handled in the data stream"; | |
| return; | |
| } | |
| auto* subscribe = absl::down_cast<LiveSubscriber*>(track); | |
| if (subscribe->visitor() != nullptr) { | |
| subscribe->visitor()->OnMalformedTrack(track->full_track_name()); | |
| } | |
| subscribe->request_stream()->Reset(kResetCodeMalformedTrack); | |
| } | |
| void MoqtSession::CleanUpState() { | |
| if (is_closing_) { | |
| return; | |
| } | |
| is_closing_ = true; | |
| if (goaway_timeout_alarm_ != nullptr) { | |
| goaway_timeout_alarm_->PermanentCancel(); | |
| } | |
| // Although PUBLISH_NAMESPACE state will be cleaned up/ by the owning stream, | |
| // the session can be destroyed first. In this case, the application callbacks | |
| // will be inaccessible. Instead, invoke application callbacks now. | |
| while (!publish_namespace_responses_.empty()) { | |
| auto it = publish_namespace_responses_.begin(); | |
| MoqtBidiStreamBase* stream = it->second; | |
| publish_namespace_responses_.erase(it); | |
| stream->Detach(); | |
| } | |
| while (!publish_namespace_requests_.empty()) { | |
| auto it = publish_namespace_requests_.begin(); | |
| MoqtBidiStreamBase* stream = it->second; | |
| publish_namespace_requests_.erase(it); | |
| stream->Detach(); | |
| } | |
| for (auto& [track_name, subscriber] : subscribe_by_name_) { | |
| // It's possible the application is going to destroy its visitor as early | |
| // as session_deleted_callback is called. So call OnPublishDone() now and | |
| // clear the visitor. | |
| if (subscriber->visitor() != nullptr) { | |
| subscriber->visitor()->OnPublishDone(subscriber->full_track_name()); | |
| subscriber->ReleaseVisitor(); | |
| } | |
| } | |
| } | |
| void MoqtSessionParameters::ToSetupOptions(SetupOptions& out) const { | |
| if (perspective == quic::Perspective::IS_CLIENT && !using_webtrans) { | |
| out.path = path; | |
| out.authority = authority; | |
| } | |
| if (max_auth_token_cache_size != kDefaultMaxAuthTokenCacheSize) { | |
| out.max_auth_token_cache_size = max_auth_token_cache_size; | |
| } | |
| if (support_object_acks != kDefaultSupportObjectAcks) { | |
| out.support_object_acks = support_object_acks; | |
| } | |
| if (!moqt_implementation.empty()) { | |
| out.moqt_implementation = moqt_implementation; | |
| } | |
| for (const AuthToken& token : authorization_token) { | |
| out.authorization_tokens.push_back(token); | |
| } | |
| } | |
| } // namespace moqt |