diff --git a/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.cpp b/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.cpp index 0d4ed635d..6b775a6d7 100644 --- a/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.cpp +++ b/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.cpp @@ -10,8 +10,9 @@ #include "../../../../../api_private.h" #include -#include -#include +#include +#include +#include namespace api { @@ -25,6 +26,7 @@ namespace api RegisterPost(R"((startHlsDump))", &StreamActionsController::OnPostStartHLSDump); RegisterPost(R"((stopHlsDump))", &StreamActionsController::OnPostStopHLSDump); RegisterPost(R"((sendEvent))", &StreamActionsController::OnPostSendEvent); + RegisterPost(R"((sendEvents))", &StreamActionsController::OnPostSendEvents); RegisterPost(R"((concludeHlsLive))", &StreamActionsController::OnPostConcludeHlsLive); } @@ -218,6 +220,42 @@ namespace api return {http::StatusCode::OK}; } + // POST /v1/vhosts//apps//streams/:injectHLSEvent + ApiResponse StreamActionsController::OnPostSendEvents(const std::shared_ptr &client, const Json::Value &request_body, + const std::shared_ptr &vhost, + const std::shared_ptr &app, + const std::shared_ptr &stream, + const std::vector> &output_streams) + { + if (request_body.isArray() == false || request_body.size() == 0) + { + throw http::HttpError(http::StatusCode::BadRequest, "events(array) is required"); + } + + MultipleStatus status_codes; + Json::Value response_value(Json::ValueType::arrayValue); + for (const auto &request_event : request_body) + { + try + { + OnPostSendEvent(client, request_event, vhost, app, stream, output_streams); + status_codes.AddStatusCode(http::StatusCode::OK); + + Json::Value response; + response["statusCode"] = static_cast(http::StatusCode::OK); + response["message"] = StringFromStatusCode(http::StatusCode::OK); + response_value.append(response); + } + catch (const http::HttpError &error) + { + status_codes.AddStatusCode(error.GetStatusCode()); + response_value.append(::serdes::JsonFromError(error)); + } + } + + return {status_codes, std::move(response_value)}; + } + // POST /v1/vhosts//apps//streams/:injectHLSEvent ApiResponse StreamActionsController::OnPostSendEvent(const std::shared_ptr &client, const Json::Value &request_body, const std::shared_ptr &vhost, @@ -226,6 +264,8 @@ namespace api const std::vector> &output_streams) { // Validate request body + + // ID3 Event // { // "eventFormat": "id3v2", // "eventType": "video", @@ -249,79 +289,48 @@ namespace api // ] // } + // Cue Event + // { + // "eventFormat": "cue", + // "events":[ + // { + // "cueType": "out", // out | in + // "duration": 60500 // milliseconds, only available when cueType is out + // } + // ] + // } + if (request_body.isMember("eventFormat") == false || request_body["eventFormat"].isString() == false || - request_body.isMember("events") == false || request_body["events"].isArray() == false) + request_body.isMember("events") == false || request_body["events"].isArray() == false || request_body["events"].size() == 0) { throw http::HttpError(http::StatusCode::BadRequest, "eventFormat(string) and events(array) are required"); } - // Now only support ID3v2 format + cmn::BitstreamFormat event_format = cmn::BitstreamFormat::Unknown; ov::String event_format_string = request_body["eventFormat"].asString().c_str(); - if (event_format_string.UpperCaseString() != "ID3V2") + std::shared_ptr events_data = nullptr; + + if (event_format_string.UpperCaseString() == "ID3V2") { - throw http::HttpError(http::StatusCode::BadRequest, "eventFormat is not supported: [%s]", event_format_string.CStr()); + event_format = cmn::BitstreamFormat::ID3v2; + events_data = MakeID3Data(request_body["events"]); } - - cmn::BitstreamFormat event_format = cmn::BitstreamFormat::ID3v2; - bool urgent = false; - - if (request_body.isMember("urgent") == true && request_body["urgent"].isBool() == true) + else if (event_format_string.UpperCaseString() == "CUE") { - urgent = request_body["urgent"].asBool(); + event_format = cmn::BitstreamFormat::CUE; + events_data = MakeCueData(request_body["events"]); } - - auto events = request_body["events"]; - if (events.size() == 0) + else { - throw http::HttpError(http::StatusCode::BadRequest, "events is empty"); + throw http::HttpError(http::StatusCode::BadRequest, "eventFormat is not supported: [%s]", event_format_string.CStr()); } - // Make ID3v2 tags - auto id3v2_event = std::make_shared(); - id3v2_event->SetVersion(4, 0); - for (const auto &event : events) + if (events_data == nullptr) { - if (event.isMember("frameType") == false || event["frameType"].isString() == false) - { - throw http::HttpError(http::StatusCode::BadRequest, "frameType is required in events"); - } - - ov::String frame_type = event["frameType"].asString().c_str(); - ov::String info; - ov::String data; - - if (event.isMember("info") == true && event["info"].isString() == true) - { - info = event["info"].asString().c_str(); - } - - if (event.isMember("data") == true && event["data"].isString() == true) - { - data = event["data"].asString().c_str(); - } - - std::shared_ptr frame; - if (frame_type.UpperCaseString() == "TXXX") - { - frame = std::make_shared(info, data); - } - else if (frame_type.UpperCaseString().Get(0) == 'T') - { - frame = std::make_shared(frame_type, data); - } - else if (frame_type.UpperCaseString() == "PRIV") - { - frame = std::make_shared(info, data); - } - else - { - throw http::HttpError(http::StatusCode::BadRequest, "frameType is not supported: [%s]", frame_type.CStr()); - } - - id3v2_event->AddFrame(frame); + throw http::HttpError(http::StatusCode::BadRequest, "Could not make events data"); } - // Event Type + // Event Type (Optional) cmn::PacketType event_type = cmn::PacketType::EVENT; if (request_body.isMember("eventType") == true) { @@ -349,7 +358,14 @@ namespace api throw http::HttpError(http::StatusCode::BadRequest, "eventType is not supported: [%s]", event_type_string.CStr()); } } - + + // Urgent (Optional) + bool urgent = false; + if (request_body.isMember("urgent") == true && request_body["urgent"].isBool() == true) + { + urgent = request_body["urgent"].asBool(); + } + auto source_stream = GetSourceStream(stream); if (source_stream == nullptr) { @@ -358,7 +374,7 @@ namespace api vhost->GetName().CStr(), app->GetVHostAppName().GetAppName().CStr(), stream->GetName().CStr()); } - if (source_stream->SendDataFrame(-1, event_format, event_type, id3v2_event->Serialize(), urgent) == false) + if (source_stream->SendDataFrame(-1, event_format, event_type, events_data, urgent) == false) { throw http::HttpError(http::StatusCode::InternalServerError, "Internal Server Error - Could not inject event: [%s/%s/%s]", @@ -473,5 +489,88 @@ namespace api return application->GetStreamByName(stream->GetName()); } - } -} \ No newline at end of file + + std::shared_ptr StreamActionsController::MakeID3Data(const Json::Value &events) + { + // Make ID3v2 tags + auto id3v2_event = std::make_shared(); + id3v2_event->SetVersion(4, 0); + for (const auto &event : events) + { + if (event.isMember("frameType") == false || event["frameType"].isString() == false) + { + throw http::HttpError(http::StatusCode::BadRequest, "frameType is required in events"); + } + + ov::String frame_type = event["frameType"].asString().c_str(); + ov::String info; + ov::String data; + + if (event.isMember("info") == true && event["info"].isString() == true) + { + info = event["info"].asString().c_str(); + } + + if (event.isMember("data") == true && event["data"].isString() == true) + { + data = event["data"].asString().c_str(); + } + + std::shared_ptr frame; + if (frame_type.UpperCaseString() == "TXXX") + { + frame = std::make_shared(info, data); + } + else if (frame_type.UpperCaseString().Get(0) == 'T') + { + frame = std::make_shared(frame_type, data); + } + else if (frame_type.UpperCaseString() == "PRIV") + { + frame = std::make_shared(info, data); + } + else + { + throw http::HttpError(http::StatusCode::BadRequest, "frameType is not supported: [%s]", frame_type.CStr()); + } + + id3v2_event->AddFrame(frame); + } + + return id3v2_event->Serialize(); + } + + std::shared_ptr StreamActionsController::MakeCueData(const Json::Value &events) + { + if (events.size() == 0) + { + throw http::HttpError(http::StatusCode::BadRequest, "events must have at least one event"); + } + + // only first event is used + auto event = events[0]; + if (event.isMember("cueType") == false || event["cueType"].isString() == false) + { + throw http::HttpError(http::StatusCode::BadRequest, "cueType is required in events"); + } + + ov::String cue_type = event["cueType"].asString().c_str(); + CueEvent::CueType type = CueEvent::GetCueTypeByName(cue_type); + if (type == CueEvent::CueType::Unknown) + { + throw http::HttpError(http::StatusCode::BadRequest, "cueType is not supported: [%s]", cue_type.CStr()); + } + + // duration (optional) + uint32_t duration_msec = 0; + if (event.isMember("duration") == true && event["duration"].isUInt() == true) + { + duration_msec = event["duration"].asUInt(); + } + + auto cue_event = CueEvent::Create(type, duration_msec); + + return cue_event->Serialize(); + } + } // namespace v1 +} // namespace api \ No newline at end of file diff --git a/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.h b/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.h index 1b665f58e..ff4979b04 100644 --- a/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.h +++ b/src/projects/api_server/controllers/v1/vhosts/apps/streams/stream_actions_controller.h @@ -46,6 +46,13 @@ namespace api const std::shared_ptr &app, const std::shared_ptr &stream, const std::vector> &output_streams); + + // POST /v1/vhosts//apps//streams/:sendEvent + ApiResponse OnPostSendEvents(const std::shared_ptr &client, const Json::Value &request_body, + const std::shared_ptr &vhost, + const std::shared_ptr &app, + const std::shared_ptr &stream, + const std::vector> &output_streams); // POST /v1/vhosts//apps//streams/:concludeHlsLive ApiResponse OnPostConcludeHlsLive(const std::shared_ptr &client, const Json::Value &request_body, @@ -89,6 +96,10 @@ namespace api return std::static_pointer_cast(stream); } + + + std::shared_ptr MakeID3Data(const Json::Value &events); // ID3v2 + std::shared_ptr MakeCueData(const Json::Value &events); // CUE }; } } \ No newline at end of file diff --git a/src/projects/base/mediarouter/media_type.h b/src/projects/base/mediarouter/media_type.h index 1fc933935..977ecf55b 100644 --- a/src/projects/base/mediarouter/media_type.h +++ b/src/projects/base/mediarouter/media_type.h @@ -48,7 +48,9 @@ namespace cmn MP3, - OVEN_EVENT // OvenMediaEngine defined event + OVEN_EVENT, // OvenMediaEngine defined event + + CUE }; enum class PacketType : int8_t diff --git a/src/projects/main/AMS.mk b/src/projects/main/AMS.mk index 9c81e04bc..8ed5d5c9c 100644 --- a/src/projects/main/AMS.mk +++ b/src/projects/main/AMS.mk @@ -30,6 +30,7 @@ LOCAL_STATIC_LIBRARIES := \ rtp_rtcp \ sdp \ id3v2 \ + cue_event \ segment_writer \ web_console \ mediarouter \ diff --git a/src/projects/modules/containers/bmff/bmff_packager.cpp b/src/projects/modules/containers/bmff/bmff_packager.cpp index c86042599..f3d9ca89c 100644 --- a/src/projects/modules/containers/bmff/bmff_packager.cpp +++ b/src/projects/modules/containers/bmff/bmff_packager.cpp @@ -9,8 +9,8 @@ #include "bmff_packager.h" #include "bmff_private.h" -#include -#include +#include +#include namespace bmff { Packager::Packager(const std::shared_ptr &media_track, const std::shared_ptr &data_track, const CencProperty &cenc_property) diff --git a/src/projects/modules/containers/bmff/fmp4_packager/fmp4_packager.cpp b/src/projects/modules/containers/bmff/fmp4_packager/fmp4_packager.cpp index 7d76837d9..2fa4a6617 100644 --- a/src/projects/modules/containers/bmff/fmp4_packager/fmp4_packager.cpp +++ b/src/projects/modules/containers/bmff/fmp4_packager/fmp4_packager.cpp @@ -13,8 +13,8 @@ #include #include -#include -#include +#include +#include namespace bmff { diff --git a/src/projects/modules/containers/mpegts/mpegts_packetizer.cpp b/src/projects/modules/containers/mpegts/mpegts_packetizer.cpp index befa4e4cc..1d5d9aa29 100644 --- a/src/projects/modules/containers/mpegts/mpegts_packetizer.cpp +++ b/src/projects/modules/containers/mpegts/mpegts_packetizer.cpp @@ -160,6 +160,11 @@ namespace mpegts return false; } + if (media_packet->GetMediaType() == cmn::MediaType::Data && media_packet->GetBitstreamFormat() != cmn::BitstreamFormat::ID3v2) + { + return false; + } + auto pid = GetElementaryPid(media_packet->GetTrackId()); auto pes = Pes::Build(pid, track, media_packet); if (pes == nullptr) diff --git a/src/projects/modules/data_format/AMS.mk b/src/projects/modules/data_format/AMS.mk new file mode 100644 index 000000000..b757d5af6 --- /dev/null +++ b/src/projects/modules/data_format/AMS.mk @@ -0,0 +1,3 @@ +LOCAL_PATH := $(call get_local_path) + +include $(BUILD_SUB_AMS) \ No newline at end of file diff --git a/src/projects/modules/data_format/cue_event/AMS.mk b/src/projects/modules/data_format/cue_event/AMS.mk new file mode 100644 index 000000000..c0bf662c3 --- /dev/null +++ b/src/projects/modules/data_format/cue_event/AMS.mk @@ -0,0 +1,8 @@ +LOCAL_PATH := $(call get_local_path) +include $(DEFAULT_VARIABLES) + +LOCAL_TARGET := cue_event + +$(call add_pkg_config,srt) + +include $(BUILD_STATIC_LIBRARY) \ No newline at end of file diff --git a/src/projects/modules/data_format/cue_event/cue_event.cpp b/src/projects/modules/data_format/cue_event/cue_event.cpp new file mode 100644 index 000000000..7a1da3d8b --- /dev/null +++ b/src/projects/modules/data_format/cue_event/cue_event.cpp @@ -0,0 +1,59 @@ +//============================================================================== +// +// OvenMediaEngine +// +// Created by Getroot +// Copyright (c) 2024 AirenSoft. All rights reserved. +// +//============================================================================== +#include "cue_event.h" + +std::shared_ptr CueEvent::Create(CueType cue_type, uint32_t duration_sec) +{ + return std::make_shared(cue_type, duration_sec); +} + +std::shared_ptr CueEvent::Parse(const std::shared_ptr &data) +{ + if (data == nullptr) + { + return nullptr; + } + + ov::ByteStream stream(data); + + CueType cue_type = static_cast(stream.Read8()); + uint32_t duration_msec = stream.ReadBE32(); + + return Create(cue_type, duration_msec); +} + +CueEvent::CueType CueEvent::GetCueTypeByName(ov::String type) +{ + if (type.UpperCaseString() == "OUT") + { + return CueType::OUT; + } + else if (type.UpperCaseString() == "IN") + { + return CueType::IN; + } + + return CueType::Unknown; +} + +CueEvent::CueEvent(CueType cue_type, uint32_t duration_sec) +{ + _cue_type = cue_type; + _duration_msec = duration_sec; +} + +std::shared_ptr CueEvent::Serialize() const +{ + ov::ByteStream stream(5); + + stream.Write8(static_cast(_cue_type)); + stream.WriteBE32(_duration_msec); + + return stream.GetDataPointer(); +} \ No newline at end of file diff --git a/src/projects/modules/data_format/cue_event/cue_event.h b/src/projects/modules/data_format/cue_event/cue_event.h new file mode 100644 index 000000000..f7e045b7d --- /dev/null +++ b/src/projects/modules/data_format/cue_event/cue_event.h @@ -0,0 +1,43 @@ +//============================================================================== +// +// OvenMediaEngine +// +// Created by Getroot +// Copyright (c) 2024 AirenSoft. All rights reserved. +// +//============================================================================== +#pragma once + +#include + +/* +Data Structure + +CueType : 8 bits +Duration - 32 bits / milliseconds, only positive value + +*/ + +class CueEvent +{ +public: + enum class CueType : uint8_t + { + Unknown = 0, + OUT, + IN + }; + + static std::shared_ptr Create(CueType cue_type, uint32_t duration_msec); + static std::shared_ptr Parse(const std::shared_ptr &data); + static CueType GetCueTypeByName(ov::String type); + + CueEvent(CueType cue_type, uint32_t duration_sec); + ~CueEvent() = default; + + std::shared_ptr Serialize() const; + +private: + CueType _cue_type; + uint32_t _duration_msec; +}; \ No newline at end of file diff --git a/src/projects/modules/id3v2/AMS.mk b/src/projects/modules/data_format/id3v2/AMS.mk similarity index 100% rename from src/projects/modules/id3v2/AMS.mk rename to src/projects/modules/data_format/id3v2/AMS.mk diff --git a/src/projects/modules/id3v2/frames/id3v2_frames.h b/src/projects/modules/data_format/id3v2/frames/id3v2_frames.h similarity index 100% rename from src/projects/modules/id3v2/frames/id3v2_frames.h rename to src/projects/modules/data_format/id3v2/frames/id3v2_frames.h diff --git a/src/projects/modules/id3v2/frames/id3v2_priv_frame.h b/src/projects/modules/data_format/id3v2/frames/id3v2_priv_frame.h similarity index 100% rename from src/projects/modules/id3v2/frames/id3v2_priv_frame.h rename to src/projects/modules/data_format/id3v2/frames/id3v2_priv_frame.h diff --git a/src/projects/modules/id3v2/frames/id3v2_text_frame.h b/src/projects/modules/data_format/id3v2/frames/id3v2_text_frame.h similarity index 100% rename from src/projects/modules/id3v2/frames/id3v2_text_frame.h rename to src/projects/modules/data_format/id3v2/frames/id3v2_text_frame.h diff --git a/src/projects/modules/id3v2/frames/id3v2_txxx_frame.h b/src/projects/modules/data_format/id3v2/frames/id3v2_txxx_frame.h similarity index 100% rename from src/projects/modules/id3v2/frames/id3v2_txxx_frame.h rename to src/projects/modules/data_format/id3v2/frames/id3v2_txxx_frame.h diff --git a/src/projects/modules/id3v2/id3v2.cpp b/src/projects/modules/data_format/id3v2/id3v2.cpp similarity index 100% rename from src/projects/modules/id3v2/id3v2.cpp rename to src/projects/modules/data_format/id3v2/id3v2.cpp diff --git a/src/projects/modules/id3v2/id3v2.h b/src/projects/modules/data_format/id3v2/id3v2.h similarity index 100% rename from src/projects/modules/id3v2/id3v2.h rename to src/projects/modules/data_format/id3v2/id3v2.h diff --git a/src/projects/modules/id3v2/id3v2_frame.cpp b/src/projects/modules/data_format/id3v2/id3v2_frame.cpp similarity index 100% rename from src/projects/modules/id3v2/id3v2_frame.cpp rename to src/projects/modules/data_format/id3v2/id3v2_frame.cpp diff --git a/src/projects/modules/id3v2/id3v2_frame.h b/src/projects/modules/data_format/id3v2/id3v2_frame.h similarity index 100% rename from src/projects/modules/id3v2/id3v2_frame.h rename to src/projects/modules/data_format/id3v2/id3v2_frame.h diff --git a/src/projects/providers/rtmp/rtmp_stream.cpp b/src/projects/providers/rtmp/rtmp_stream.cpp index 331ac8607..ca8c8105a 100644 --- a/src/projects/providers/rtmp/rtmp_stream.cpp +++ b/src/projects/providers/rtmp/rtmp_stream.cpp @@ -13,8 +13,8 @@ #include #include #include -#include -#include +#include +#include #include #include "base/info/application.h"