commit 85685e7e17dcd3f717e35d046acf188eac910e53 Author: Daniel Dada Date: Tue Mar 3 08:13:16 2026 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7194ea7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cache +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..484d03b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.16) +project(Eclipt VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +option(ECLIPT_BUILD_SHARED "Build shared library" ON) +option(ECLIPT_BUILD_EXAMPLES "Build examples" OFF) +option(ECLIPT_USE_SDL2 "Use SDL2 for Linux display" ON) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(FFMPEG REQUIRED + libavformat + libavcodec + libavutil + libswscale + libswresample +) + +if(ECLIPT_USE_SDL2) + pkg_check_modules(SDL2 REQUIRED sdl2) +endif() + +include_directories( + ${CMAKE_SOURCE_DIR}/libEcliptPlayer/include + ${CMAKE_SOURCE_DIR}/platform/linux/include + ${FFMPEG_INCLUDE_DIRS} +) + +if(ECLIPT_USE_SDL2) + include_directories(${SDL2_INCLUDE_DIRS}) +endif() + +set(ECLIPT_SOURCES + libEcliptPlayer/src/core/EcliptPlayer.cpp + libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp + libEcliptPlayer/src/decoder/FFmpegDecoder.cpp + libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp + libEcliptPlayer/src/playlist/Playlist.cpp + libEcliptPlayer/src/playlist/EPG.cpp +) + +set(ECLIPT_HEADERS + libEcliptPlayer/include/eclipt/Config.h + libEcliptPlayer/include/eclipt/Decoder.h + libEcliptPlayer/include/eclipt/Demuxer.h + libEcliptPlayer/include/eclipt/EcliptPlayer.h + libEcliptPlayer/include/eclipt/EPG.h + libEcliptPlayer/include/eclipt/Frame.h + libEcliptPlayer/include/eclipt/Playlist.h + libEcliptPlayer/include/eclipt/Subtitle.h +) + +add_library(EcliptPlayer ${ECLIPT_SOURCES} ${ECLIPT_HEADERS}) +target_link_libraries(EcliptPlayer PRIVATE ${FFMPEG_LIBRARIES}) +target_compile_options(EcliptPlayer PRIVATE ${FFMPEG_CFLAGS}) + +if(WIN32) + target_compile_definitions(EcliptPlayer PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +set_target_properties(EcliptPlayer PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + PUBLIC_HEADER "${ECLIPT_HEADERS}" +) + +if(UNIX AND NOT APPLE) + install(TARGETS EcliptPlayer + LIBRARY DESTINATION lib + PUBLIC_HEADER DESTINATION include/eclipt + ) +endif() + +if(ECLIPT_USE_SDL2) + set(LINUX_SOURCES + platform/linux/src/EcliptLinux.cpp + platform/linux/src/main.cpp + ) + + add_executable(eclipt-linux ${LINUX_SOURCES}) + target_link_libraries(eclipt-linux PRIVATE + EcliptPlayer + ${SDL2_LIBRARIES} + pthread + ) +endif() + +if(ECLIPT_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +message(STATUS "Eclipt version: ${PROJECT_VERSION}") +message(STATUS "FFmpeg version: ${FFMPEG_VERSION}") +message(STATUS "Build shared: ${ECLIPT_BUILD_SHARED}") +message(STATUS "Use SDL2: ${ECLIPT_USE_SDL2}") diff --git a/libEcliptPlayer/include/eclipt/Config.h b/libEcliptPlayer/include/eclipt/Config.h new file mode 100644 index 0000000..59dadb5 --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Config.h @@ -0,0 +1,61 @@ +#ifndef ECLIPT_CONFIG_H +#define ECLIPT_CONFIG_H + +#include "Frame.h" +#include +#include +#include +#include + +namespace eclipt { + +struct DecoderConfig { + bool use_hw_acceleration = true; + std::string hw_device = "auto"; + int thread_count = 0; + bool enable_frame_skiping = false; + int max_ref_frames = 2; + bool low_latency = false; +}; + +struct OutputConfig { + PixelFormat preferred_format = PixelFormat::YUV420; + bool enable_frame_interpolation = false; + int target_fps = 0; + int frame_pool_size = 8; + bool vsync = true; +}; + +struct StreamConfig { + int video_stream_index = -1; + int audio_stream_index = -1; + int subtitle_stream_index = -1; + std::string preferred_language = "eng"; + bool strict_stream_selection = false; +}; + +struct PlayerConfig { + DecoderConfig decoder; + OutputConfig output; + StreamConfig stream; + int buffer_size_ms = 1000; + bool prebuffer = true; +}; + +enum class LogLevel { + Debug, + Info, + Warning, + Error +}; + +enum class SeekDirection { + Forward, + Backward, + Absolute +}; + +using LogCallback = std::function; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/Decoder.h b/libEcliptPlayer/include/eclipt/Decoder.h new file mode 100644 index 0000000..7e81567 --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Decoder.h @@ -0,0 +1,156 @@ +#ifndef ECLIPT_DECODER_H +#define ECLIPT_DECODER_H + +#include "Frame.h" +#include "Demuxer.h" +#include "Config.h" +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#ifdef __cplusplus +} +#endif + +namespace eclipt { + +enum class DecoderType { + Software, + Hardware, + Auto +}; + +enum class HardwareAPI { + None, + VAAPI, + VDPAU, + CUDA, + D3D11VA, + VideoToolbox, + MediaCodec, + MMAL, + OMX +}; + +struct DecoderCapabilities { + bool supports_hw_decode = false; + bool supports_hw_encode = false; + std::vector supported_apis; + std::vector supported_codecs; + int max_resolution_width = 0; + int max_resolution_height = 0; + bool supports_10bit = false; + bool supports_hdr = false; +}; + +struct DecodeStats { + int decoded_frames = 0; + int dropped_frames = 0; + int corrupted_frames = 0; + int64_t decode_time_us = 0; + double fps = 0.0; + bool is_hardware = false; + HardwareAPI hw_api = HardwareAPI::None; +}; + +class IDecoder { +public: + virtual ~IDecoder() = default; + + virtual bool open(const StreamMetadata& stream, const DecoderConfig& config) = 0; + virtual void close() = 0; + virtual bool isOpen() const = 0; + + virtual bool sendPacket(const Packet& packet) = 0; + virtual bool receiveFrame(VideoFrame& frame) = 0; + virtual bool flush() = 0; + + virtual DecoderType getDecoderType() const = 0; + virtual HardwareAPI getHardwareAPI() const = 0; + virtual const DecodeStats& getStats() const = 0; + + virtual void setHardwareAPI(HardwareAPI api) = 0; + virtual bool supportsFormat(const std::string& codec) const = 0; + + virtual std::string getLastError() const = 0; +}; + +class FFmpegDecoder : public IDecoder { +public: + FFmpegDecoder(); + ~FFmpegDecoder() override; + + bool open(const StreamMetadata& stream, const DecoderConfig& config) override; + void close() override; + bool isOpen() const override; + + bool sendPacket(const Packet& packet) override; + bool receiveFrame(VideoFrame& frame) override; + bool flush() override; + + DecoderType getDecoderType() const override; + HardwareAPI getHardwareAPI() const override; + const DecodeStats& getStats() const override; + + void setHardwareAPI(HardwareAPI api) override; + bool supportsFormat(const std::string& codec) const override; + + std::string getLastError() const override; + + static DecoderCapabilities getCapabilities(); + static std::vector getAvailableHardwareAPIs(); + static HardwareAPI probeBestHardwareAPI(const std::string& codec); + + void* getHardwareDeviceContext() const; + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +class FramePool : public IFramePool { +public: + FramePool(size_t max_size, PixelFormat format, uint32_t width, uint32_t height); + ~FramePool() override; + + VideoFrame acquire() override; + void release(VideoFrame&& frame) override; + void clear() override; + size_t available() const override; + + void setFormat(PixelFormat format); + void setDimensions(uint32_t width, uint32_t height); + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +class DecoderPool { +public: + DecoderPool(); + ~DecoderPool(); + + void setMaxDecoders(size_t count); + size_t getMaxDecoders() const; + + std::shared_ptr acquire(const StreamMetadata& stream, const DecoderConfig& config); + void release(std::shared_ptr decoder); + + void clear(); + size_t activeCount() const; + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/Demuxer.h b/libEcliptPlayer/include/eclipt/Demuxer.h new file mode 100644 index 0000000..27a1deb --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Demuxer.h @@ -0,0 +1,149 @@ +#ifndef ECLIPT_DEMUXER_H +#define ECLIPT_DEMUXER_H + +#include "Frame.h" +#include "Config.h" +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#ifdef __cplusplus +} +#endif + +namespace eclipt { + +enum class MediaType { + Video, + Audio, + Subtitle, + Data, + Attachment +}; + +struct StreamMetadata { + int stream_index = -1; + MediaType media_type = MediaType::Video; + std::string codec_name; + std::string codec_long_name; + std::string language; + std::string title; + int profile = 0; + int level = 0; + int width = 0; + int height = 0; + int64_t bitrate = 0; + int sample_rate = 0; + int channels = 0; + std::string channel_layout; + int fps_num = 0; + int fps_den = 1; + int64_t duration = 0; + bool is_default = false; + bool is_forced = false; +}; + +struct Packet { + std::vector data; + int stream_index = -1; + int64_t pts = AV_NOPTS_VALUE; + int64_t dts = AV_NOPTS_VALUE; + int64_t duration = 0; + int flags = 0; + MediaType media_type = MediaType::Video; + + Packet() = default; + + bool isKeyframe() const { return flags & AV_PKT_FLAG_KEY; } + bool isCorrupt() const { return flags & AV_PKT_FLAG_CORRUPT; } + + void clear() { + data.clear(); + stream_index = -1; + pts = AV_NOPTS_VALUE; + dts = AV_NOPTS_VALUE; + duration = 0; + flags = 0; + } +}; + +class IDemuxer { +public: + virtual ~IDemuxer() = default; + + virtual bool open(const std::string& url, const std::string& mime_type = "") = 0; + virtual void close() = 0; + virtual bool isOpen() const = 0; + + virtual bool seek(int64_t timestamp_ms, eclipt::SeekDirection dir = eclipt::SeekDirection::Absolute) = 0; + virtual bool seekToKeyframe(int64_t timestamp_ms) = 0; + + virtual bool readPacket(Packet& packet) = 0; + virtual int getPacketCount() const = 0; + + virtual std::vector getStreams() const = 0; + virtual const StreamMetadata* getStream(int index) const = 0; + virtual const StreamMetadata* getBestVideoStream() const = 0; + virtual const StreamMetadata* getBestAudioStream() const = 0; + virtual const StreamMetadata* getBestSubtitleStream() const = 0; + + virtual int64_t getDuration() const = 0; + virtual int64_t getStartTime() const = 0; + virtual int64_t getBitrate() const = 0; + + virtual std::string getUrl() const = 0; + virtual std::string getFormatName() const = 0; + + virtual void setPreferredStream(int media_type, int stream_index) = 0; + + using ReadCallback = std::function; + virtual void setReadCallback(ReadCallback callback) = 0; +}; + +class FFmpegDemuxer : public IDemuxer { +public: + FFmpegDemuxer(); + ~FFmpegDemuxer() override; + + bool open(const std::string& url, const std::string& mime_type = "") override; + void close() override; + bool isOpen() const override; + + bool seek(int64_t timestamp_ms, eclipt::SeekDirection dir = eclipt::SeekDirection::Absolute) override; + bool seekToKeyframe(int64_t timestamp_ms) override; + + bool readPacket(Packet& packet) override; + int getPacketCount() const override; + + std::vector getStreams() const override; + const StreamMetadata* getStream(int index) const override; + const StreamMetadata* getBestVideoStream() const override; + const StreamMetadata* getBestAudioStream() const override; + const StreamMetadata* getBestSubtitleStream() const override; + + int64_t getDuration() const override; + int64_t getStartTime() const override; + int64_t getBitrate() const override; + + std::string getUrl() const override; + std::string getFormatName() const override; + + void setPreferredStream(int media_type, int stream_index) override; + void setReadCallback(ReadCallback callback) override; + + void* getAVFormatContext() const; + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +} + +#endif diff --git a/libEcliptPlayer/include/eclipt/EPG.h b/libEcliptPlayer/include/eclipt/EPG.h new file mode 100644 index 0000000..5a7e382 --- /dev/null +++ b/libEcliptPlayer/include/eclipt/EPG.h @@ -0,0 +1,104 @@ +#ifndef ECLIPT_EPG_H +#define ECLIPT_EPG_H + +#include +#include +#include +#include +#include +#include + +namespace eclipt { + +struct EPGEvent { + int64_t id = 0; + std::string title; + std::string description; + int64_t start_time = 0; + int64_t end_time = 0; + int64_t duration = 0; + std::string channel_id; + std::string category; + std::string icon_url; + bool is_current = false; + bool is_future = false; + bool is_past = false; +}; + +struct EPGChannel { + std::string id; + std::string name; + std::string icon_url; + std::string group; + std::vector events; + + const EPGEvent* getCurrentEvent() const; + std::vector getEventsInRange(int64_t start, int64_t end) const; +}; + +struct EPGData { + std::string source; + int64_t fetched_time = 0; + int64_t valid_from = 0; + int64_t valid_to = 0; + std::map channels; + + bool loadFromFile(const std::string& path); + bool loadFromXml(const std::string& xml_content); + bool loadFromUrl(const std::string& url); + + bool merge(const EPGData& other); + + const EPGChannel* findChannel(const std::string& id) const; + std::vector findChannels(const std::string& group) const; +}; + +class EPGParser { +public: + virtual ~EPGParser() = default; + virtual bool parse(const std::string& content, EPGData& epg) = 0; + virtual std::string getLastError() const = 0; +}; + +class XmltvParser : public EPGParser { +public: + bool parse(const std::string& content, EPGData& epg) override; + std::string getLastError() const override { return last_error_; } + +private: + std::string last_error_; + bool parseChannel(const std::string& xml, EPGChannel& channel); + bool parseProgramme(const std::string& xml, EPGEvent& event); + int64_t parseXmltvTime(const std::string& str); +}; + +class EPGFetcher { +public: + EPGFetcher(); + ~EPGFetcher(); + + void setBaseUrl(const std::string& url); + void setAuth(const std::string& username, const std::string& password); + + bool fetch(EPGData& epg, const std::string& channel_id = ""); + bool fetchAll(EPGData& epg); + + void cancel(); + bool isCancelled() const { return cancelled_; } + + using ProgressCallback = std::function; + void setProgressCallback(ProgressCallback callback); + +private: + std::string base_url_; + std::string username_; + std::string password_; + bool cancelled_ = false; + ProgressCallback progress_callback_; + + std::string buildUrl(const std::string& channel_id); + bool authenticate(); +}; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/EcliptPlayer.h b/libEcliptPlayer/include/eclipt/EcliptPlayer.h new file mode 100644 index 0000000..205497a --- /dev/null +++ b/libEcliptPlayer/include/eclipt/EcliptPlayer.h @@ -0,0 +1,102 @@ +#ifndef ECLIPT_PLAYER_H +#define ECLIPT_PLAYER_H + +#include "Config.h" +#include "Frame.h" +#include "Playlist.h" +#include "EPG.h" +#include "Decoder.h" +#include +#include +#include +#include + +namespace eclipt { + +enum class PlayerState { + Stopped, + Opening, + Buffering, + Playing, + Paused, + Error +}; + +struct PlayerStats { + int64_t current_pts = 0; + int64_t buffer_duration_ms = 0; + int dropped_frames = 0; + int decoded_frames = 0; + double fps = 0.0; + int network_bitrate = 0; + bool is_hardware_decoding = false; +}; + +class IPlatformHooks { +public: + virtual ~IPlatformHooks() = default; + virtual void* createHardwareContext() = 0; + virtual void destroyHardwareContext(void* ctx) = 0; + virtual bool uploadToTexture(const VideoFrame& frame, void* texture) = 0; + virtual bool supportsHardwareDecode(const char* codec) const = 0; + virtual std::string getPreferredDecoder() const = 0; +}; + +class EcliptPlayer { +public: + EcliptPlayer(); + ~EcliptPlayer(); + + void setConfig(const PlayerConfig& config); + PlayerConfig getConfig() const; + + void setPlatformHooks(std::unique_ptr hooks); + + void setLogCallback(LogCallback callback); + void setVideoCallback(FrameCallback callback); + void setAudioCallback(AudioCallback callback); + + bool open(const std::string& url); + bool open(const std::string& url, const std::string& mime_type); + void close(); + + bool play(); + bool pause(); + bool stop(); + + bool seek(int64_t timestamp_ms, SeekDirection dir = SeekDirection::Absolute); + bool seekToProgram(unsigned int program_id); + bool seekToChannel(int channel_number); + + PlayerState getState() const; + PlayerStats getStats() const; + + float getVolume() const; + void setVolume(float volume); + + bool setAudioTrack(int track_index); + bool setSubtitleTrack(int track_index); + + std::vector getAudioTracks() const; + std::vector getSubtitleTracks() const; + + bool isPlaying() const { return getState() == PlayerState::Playing; } + int64_t getDuration() const; + int64_t getCurrentPosition() const; + + VideoFrame interpolate(const VideoFrame& a, const VideoFrame& b); + VideoFrame getDecodedFrame(); + + void setInterpolationEnabled(bool enabled); + bool isInterpolationEnabled() const; + + static const char* getVersion(); + static void setGlobalLogLevel(LogLevel level); + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/Frame.h b/libEcliptPlayer/include/eclipt/Frame.h new file mode 100644 index 0000000..5238048 --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Frame.h @@ -0,0 +1,90 @@ +#ifndef ECLIPT_FRAME_H +#define ECLIPT_FRAME_H + +#include +#include +#include +#include +#include + +namespace eclipt { + +enum class PixelFormat { + YUV420, + RGB24, + NV12, + BGRA32 +}; + +enum class FrameType { + Video, + Audio, + Subtitle +}; + +struct FrameBuffer { + uint8_t* data = nullptr; + size_t size = 0; + size_t stride = 0; + + FrameBuffer() = default; + FrameBuffer(uint8_t* d, size_t s, size_t st = 0) : data(d), size(s), stride(st) {} +}; + +struct VideoFrame { + uint32_t width = 0; + uint32_t height = 0; + PixelFormat format = PixelFormat::YUV420; + int64_t pts = 0; + int64_t dts = 0; + int64_t duration = 0; + + FrameBuffer planes[3]; + bool is_interpolated = false; + float interpolation_factor = 0.0f; + + VideoFrame() = default; + + bool isValid() const { return width > 0 && height > 0 && planes[0].data != nullptr; } + + size_t totalSize() const { + switch (format) { + case PixelFormat::YUV420: + return width * height * 3 / 2; + case PixelFormat::RGB24: + return width * height * 3; + case PixelFormat::NV12: + return width * height * 3 / 2; + case PixelFormat::BGRA32: + return width * height * 4; + default: + return 0; + } + } +}; + +struct AudioFrame { + int sample_rate = 0; + int channels = 0; + int format = 0; + int64_t pts = 0; + int64_t duration = 0; + FrameBuffer buffer; + + size_t totalSize() const { return buffer.size; } +}; + +class IFramePool { +public: + virtual ~IFramePool() = default; + virtual VideoFrame acquire() = 0; + virtual void release(VideoFrame&& frame) = 0; + virtual void clear() = 0; + virtual size_t available() const = 0; +}; + +using FrameCallback = std::function; +using AudioCallback = std::function; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/Playlist.h b/libEcliptPlayer/include/eclipt/Playlist.h new file mode 100644 index 0000000..4bc3cbb --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Playlist.h @@ -0,0 +1,78 @@ +#ifndef ECLIPT_PLAYLIST_H +#define ECLIPT_PLAYLIST_H + +#include +#include +#include +#include +#include + +namespace eclipt { + +enum class PlaylistType { + M3U, + M3U8, + PLS, + EXT +}; + +struct StreamInfo { + int index = -1; + std::string name; + std::string language; + std::string codec; + int bandwidth = 0; + std::string url; + std::string group; + bool is_default = false; + bool is_selected = false; +}; + +struct PlaylistItem { + std::string name; + std::string url; + std::string tvg_name; + std::string tvg_id; + std::string group; + int tvg_logo = 0; + int64_t duration = -1; + bool is_live = false; + std::vector streams; +}; + +struct Playlist { + std::string name; + PlaylistType type = PlaylistType::M3U; + std::vector items; + std::string base_url; + + Playlist() = default; + bool loadFromFile(const std::string& path); + bool loadFromString(const std::string& content); + bool loadFromUrl(const std::string& url); + + std::vector getLiveStreams() const; + std::vector getByGroup(const std::string& group) const; + PlaylistItem* findById(const std::string& id); +}; + +class IPlaylistParser { +public: + virtual ~IPlaylistParser() = default; + virtual bool parse(const std::string& content, Playlist& playlist) = 0; + virtual std::string getLastError() const = 0; +}; + +class M3UParser : public IPlaylistParser { +public: + bool parse(const std::string& content, Playlist& playlist) override; + std::string getLastError() const override { return last_error_; } + +private: + std::string last_error_; + bool parseExtInf(const std::string& line, PlaylistItem& item); + bool parseAttribute(const std::string& attr, std::string& key, std::string& value); +}; + +} +#endif diff --git a/libEcliptPlayer/include/eclipt/Subtitle.h b/libEcliptPlayer/include/eclipt/Subtitle.h new file mode 100644 index 0000000..69b41b2 --- /dev/null +++ b/libEcliptPlayer/include/eclipt/Subtitle.h @@ -0,0 +1,145 @@ +#ifndef ECLIPT_SUBTITLE_H +#define ECLIPT_SUBTITLE_H + +#include "Frame.h" +#include +#include +#include +#include + +namespace eclipt { + +enum class SubtitleType { + SRT, + VTT, + ASS, + SSA, + DVBSub, + DVB_Teletext, + EIA_608 +}; + +struct SubtitleCue { + int64_t start_time = 0; + int64_t end_time = 0; + std::string text; + std::vector lines; + int style_index = -1; + + bool isValid() const { return !text.empty() && end_time > start_time; } + bool isActive(int64_t pts) const { + return pts >= start_time && pts < end_time; + } +}; + +struct SubtitleStyle { + std::string font_name; + int font_size = 24; + uint32_t primary_color = 0xFFFFFFFF; + uint32_t secondary_color = 0xFF000000; + uint32_t outline_color = 0xFF000000; + uint32_t back_color = 0x00000000; + int bold = 0; + int italic = 0; + int underline = 0; + int strikeout = 0; + double margin_l = 0; + double margin_r = 0; + double margin_v = 0; + int alignment = 2; + double scale_x = 1.0; + double scale_y = 1.0; +}; + +struct RenderedSubtitle { + VideoFrame frame; + int64_t pts = 0; + int64_t duration = 0; + bool has_ass_events = false; + + RenderedSubtitle() = default; + bool isValid() const { return frame.isValid(); } +}; + +class ISubtitleRenderer { +public: + virtual ~ISubtitleRenderer() = default; + + virtual bool initialize(int width, int height) = 0; + virtual void shutdown() = 0; + + virtual bool loadSubtitles(const std::string& path) = 0; + virtual bool loadSubtitles(const uint8_t* data, size_t size, SubtitleType type) = 0; + virtual void clearSubtitles() = 0; + + virtual void setStyle(const SubtitleStyle& style) = 0; + virtual SubtitleStyle getStyle() const = 0; + + virtual RenderedSubtitle render(int64_t pts) = 0; + virtual const SubtitleCue* getCurrentCue(int64_t pts) const = 0; + + virtual bool hasSubtitles() const = 0; + virtual size_t getCueCount() const = 0; +}; + +class SubtitleDecoder { +public: + SubtitleDecoder(); + ~SubtitleDecoder(); + + bool open(const std::string& url); + bool open(const uint8_t* data, size_t size, SubtitleType type); + void close(); + + bool isOpen() const { return is_open_; } + SubtitleType getType() const { return current_type_; } + + std::vector getCues() const { return cues_; } + const SubtitleCue* getCueAtTime(int64_t pts) const; + + bool parse(); + +private: + bool is_open_ = false; + SubtitleType current_type_ = SubtitleType::SRT; + std::vector cues_; + std::vector data_; + + bool parseSRT(); + bool parseVTT(); + bool parseASS(); + + int64_t parseTimestamp(const std::string& str); + std::vector splitLines(const std::string& text); +}; + +class SubtitleRenderer : public ISubtitleRenderer { +public: + SubtitleRenderer(); + ~SubtitleRenderer() override; + + bool initialize(int width, int height) override; + void shutdown() override; + + bool loadSubtitles(const std::string& path) override; + bool loadSubtitles(const uint8_t* data, size_t size, SubtitleType type) override; + void clearSubtitles() override; + + void setStyle(const SubtitleStyle& style) override; + SubtitleStyle getStyle() const override; + + RenderedSubtitle render(int64_t pts) override; + const SubtitleCue* getCurrentCue(int64_t pts) const override; + + bool hasSubtitles() const override; + size_t getCueCount() const override; + + void setVideoParams(int width, int height, int fps_num, int fps_den); + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +} +#endif diff --git a/libEcliptPlayer/src/core/EcliptPlayer.cpp b/libEcliptPlayer/src/core/EcliptPlayer.cpp new file mode 100644 index 0000000..717c450 --- /dev/null +++ b/libEcliptPlayer/src/core/EcliptPlayer.cpp @@ -0,0 +1,455 @@ +#include "../../include/eclipt/EcliptPlayer.h" +#include "../../include/eclipt/Demuxer.h" +#include "../../include/eclipt/Decoder.h" +#include "../../include/eclipt/Subtitle.h" +#include +#include +#include +#include + +namespace eclipt { + +struct EcliptPlayer::Impl { + PlayerConfig config; + std::unique_ptr platform_hooks; + + std::unique_ptr demuxer; + std::unique_ptr video_decoder; + std::unique_ptr subtitle_renderer; + std::unique_ptr frame_pool; + + FrameCallback video_callback; + AudioCallback audio_callback; + LogCallback log_callback; + + std::atomic state{PlayerState::Stopped}; + std::atomic running{false}; + + std::thread demux_thread; + std::thread decode_thread; + std::thread output_thread; + + std::mutex packet_queue_mutex; + std::condition_variable packet_queue_cv; + std::vector packet_queue; + bool eof_reached = false; + + std::mutex frame_queue_mutex; + std::condition_variable frame_queue_cv; + std::vector decoded_frames; + VideoFrame last_frame; + VideoFrame current_frame; + + float volume = 1.0f; + bool interpolation_enabled = false; + + PlayerStats stats; + std::mutex stats_mutex; + + int64_t duration = 0; + int64_t current_position = 0; + + const StreamMetadata* video_stream = nullptr; + const StreamMetadata* audio_stream = nullptr; + + bool initComponents() { + demuxer = std::make_unique(); + video_decoder = std::make_unique(); + subtitle_renderer = std::make_unique(); + + return true; + } + + void log(LogLevel level, const std::string& msg) { + if (log_callback) { + log_callback(level, msg.c_str()); + } + } + + void setState(PlayerState new_state) { + state.store(new_state); + } + + bool openUrl(const std::string& url, const std::string& mime_type) { + setState(PlayerState::Opening); + + if (!demuxer) { + initComponents(); + } + + if (!demuxer->open(url, mime_type)) { + log(LogLevel::Error, "Failed to open URL: " + url); + setState(PlayerState::Error); + return false; + } + + video_stream = demuxer->getBestVideoStream(); + audio_stream = demuxer->getBestAudioStream(); + + if (video_stream) { + if (!video_decoder->open(*video_stream, config.decoder)) { + log(LogLevel::Error, "Failed to open video decoder"); + setState(PlayerState::Error); + return false; + } + + int pool_size = config.output.frame_pool_size; + frame_pool = std::make_unique( + pool_size, + config.output.preferred_format, + video_stream->width, + video_stream->height + ); + + subtitle_renderer->initialize(video_stream->width, video_stream->height); + } + + duration = demuxer->getDuration(); + + setState(PlayerState::Stopped); + return true; + } + + void close() { + running.store(false); + + if (demux_thread.joinable()) demux_thread.join(); + if (decode_thread.joinable()) decode_thread.join(); + if (output_thread.joinable()) output_thread.join(); + + if (demuxer) { + demuxer->close(); + } + if (video_decoder) { + video_decoder->close(); + } + + std::lock_guard lock(packet_queue_mutex); + packet_queue.clear(); + + std::lock_guard lock2(frame_queue_mutex); + decoded_frames.clear(); + + setState(PlayerState::Stopped); + } + + bool startPlayback() { + if (state.load() == PlayerState::Playing) return true; + + running.store(true); + eof_reached = false; + + demux_thread = std::thread([this]() { demuxLoop(); }); + decode_thread = std::thread([this]() { decodeLoop(); }); + output_thread = std::thread([this]() { outputLoop(); }); + + setState(PlayerState::Playing); + return true; + } + + bool pausePlayback() { + if (state.load() != PlayerState::Playing) return false; + setState(PlayerState::Paused); + return true; + } + + void demuxLoop() { + while (running.load()) { + if (state.load() == PlayerState::Paused) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + Packet packet; + if (!demuxer->readPacket(packet)) { + if (demuxer->getUrl().empty()) { + eof_reached = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + if (packet.stream_index == video_stream->stream_index || + packet.stream_index == audio_stream->stream_index) { + std::unique_lock lock(packet_queue_mutex); + packet_queue_cv.wait(lock, [this]() { + return packet_queue.size() < 50 || !running.load(); + }); + + if (running.load()) { + packet_queue.push_back(std::move(packet)); + } + } + } + } + + void decodeLoop() { + while (running.load()) { + Packet packet; + + { + std::lock_guard lock(packet_queue_mutex); + if (!packet_queue.empty()) { + packet = std::move(packet_queue.front()); + packet_queue.erase(packet_queue.begin()); + } + } + + if (packet.data.empty()) { + if (eof_reached) { + video_decoder->flush(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + + if (packet.media_type == MediaType::Video) { + if (video_decoder->sendPacket(packet)) { + VideoFrame frame; + while (video_decoder->receiveFrame(frame)) { + std::lock_guard lock(frame_queue_mutex); + decoded_frames.push_back(std::move(frame)); + frame_queue_cv.notify_one(); + } + } + } + } + } + + void outputLoop() { + while (running.load()) { + VideoFrame frame; + + { + std::unique_lock lock(frame_queue_mutex); + frame_queue_cv.wait(lock, [this]() { + return !decoded_frames.empty() || !running.load(); + }); + + if (!decoded_frames.empty()) { + frame = std::move(decoded_frames.front()); + decoded_frames.erase(decoded_frames.begin()); + } + } + + if (frame.isValid()) { + last_frame = current_frame; + current_frame = frame; + current_position = frame.pts / 1000; + + if (interpolation_enabled && last_frame.isValid()) { + VideoFrame interp_frame; + interp_frame.width = last_frame.width; + interp_frame.height = last_frame.height; + interp_frame.format = last_frame.format; + interp_frame.pts = last_frame.pts + static_cast((current_frame.pts - last_frame.pts) * 0.5); + interp_frame.is_interpolated = true; + interp_frame.interpolation_factor = 0.5f; + + if (last_frame.planes[0].data && current_frame.planes[0].data) { + size_t size = last_frame.planes[0].size; + uint8_t* buffer = new uint8_t[size]; + + for (size_t i = 0; i < size; ++i) { + int av = last_frame.planes[0].data[i]; + int bv = current_frame.planes[0].data[i]; + buffer[i] = static_cast((av + bv) / 2); + } + + interp_frame.planes[0] = FrameBuffer(buffer, size, last_frame.planes[0].stride); + } + + if (video_callback) { + video_callback(std::move(interp_frame)); + } + } else { + if (video_callback) { + video_callback(std::move(frame)); + } + } + } + + if (state.load() == PlayerState::Paused) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + } +}; + +EcliptPlayer::EcliptPlayer() : pImpl(std::make_unique()) {} + +EcliptPlayer::~EcliptPlayer() { + close(); +} + +void EcliptPlayer::setConfig(const PlayerConfig& config) { + pImpl->config = config; +} + +PlayerConfig EcliptPlayer::getConfig() const { + return pImpl->config; +} + +void EcliptPlayer::setPlatformHooks(std::unique_ptr hooks) { + pImpl->platform_hooks = std::move(hooks); +} + +void EcliptPlayer::setLogCallback(LogCallback callback) { + pImpl->log_callback = std::move(callback); +} + +void EcliptPlayer::setVideoCallback(FrameCallback callback) { + pImpl->video_callback = std::move(callback); +} + +void EcliptPlayer::setAudioCallback(AudioCallback callback) { + pImpl->audio_callback = std::move(callback); +} + +bool EcliptPlayer::open(const std::string& url) { + return pImpl->openUrl(url, ""); +} + +bool EcliptPlayer::open(const std::string& url, const std::string& mime_type) { + return pImpl->openUrl(url, mime_type); +} + +void EcliptPlayer::close() { + pImpl->close(); +} + +bool EcliptPlayer::play() { + return pImpl->startPlayback(); +} + +bool EcliptPlayer::pause() { + return pImpl->pausePlayback(); +} + +bool EcliptPlayer::stop() { + pImpl->close(); + return true; +} + +bool EcliptPlayer::seek(int64_t timestamp_ms, SeekDirection dir) { + if (!pImpl->demuxer) return false; + + bool result = false; + if (dir == SeekDirection::Absolute) { + result = pImpl->demuxer->seek(timestamp_ms, dir); + } else if (dir == SeekDirection::Forward) { + result = pImpl->demuxer->seek(timestamp_ms, SeekDirection::Absolute); + } else { + result = pImpl->demuxer->seekToKeyframe(timestamp_ms); + } + + if (result) { + std::lock_guard lock(pImpl->frame_queue_mutex); + pImpl->decoded_frames.clear(); + std::lock_guard lock2(pImpl->packet_queue_mutex); + pImpl->packet_queue.clear(); + } + + return result; +} + +bool EcliptPlayer::seekToProgram(unsigned int) { + return false; +} + +bool EcliptPlayer::seekToChannel(int) { + return false; +} + +PlayerState EcliptPlayer::getState() const { + return pImpl->state.load(); +} + +PlayerStats EcliptPlayer::getStats() const { + return pImpl->stats; +} + +float EcliptPlayer::getVolume() const { + return pImpl->volume; +} + +void EcliptPlayer::setVolume(float volume) { + pImpl->volume = std::max(0.0f, std::min(1.0f, volume)); +} + +bool EcliptPlayer::setAudioTrack(int) { + return false; +} + +bool EcliptPlayer::setSubtitleTrack(int) { + return false; +} + +std::vector EcliptPlayer::getAudioTracks() const { + return {}; +} + +std::vector EcliptPlayer::getSubtitleTracks() const { + return {}; +} + +int64_t EcliptPlayer::getDuration() const { + return pImpl->duration; +} + +int64_t EcliptPlayer::getCurrentPosition() const { + return pImpl->current_position; +} + +VideoFrame EcliptPlayer::interpolate(const VideoFrame& a, const VideoFrame& b) { + VideoFrame result; + result.width = a.width; + result.height = a.height; + result.format = a.format; + result.pts = a.pts + static_cast((b.pts - a.pts) * 0.5); + result.is_interpolated = true; + result.interpolation_factor = 0.5f; + + if (a.planes[0].data && b.planes[0].data) { + size_t size = a.planes[0].size; + uint8_t* buffer = new uint8_t[size]; + + for (size_t i = 0; i < size; ++i) { + int av = a.planes[0].data[i]; + int bv = b.planes[0].data[i]; + buffer[i] = static_cast((av + bv) / 2); + } + + result.planes[0] = FrameBuffer(buffer, size, a.planes[0].stride); + } + + return result; +} + +VideoFrame EcliptPlayer::getDecodedFrame() { + std::lock_guard lock(pImpl->frame_queue_mutex); + if (!pImpl->decoded_frames.empty()) { + VideoFrame frame = std::move(pImpl->decoded_frames.front()); + pImpl->decoded_frames.erase(pImpl->decoded_frames.begin()); + return frame; + } + return VideoFrame(); +} + +void EcliptPlayer::setInterpolationEnabled(bool enabled) { + pImpl->interpolation_enabled = enabled; +} + +bool EcliptPlayer::isInterpolationEnabled() const { + return pImpl->interpolation_enabled; +} + +const char* EcliptPlayer::getVersion() { + return "EcliptPlayer 1.0.0"; +} + +void EcliptPlayer::setGlobalLogLevel(LogLevel) { +} + +} diff --git a/libEcliptPlayer/src/decoder/FFmpegDecoder.cpp b/libEcliptPlayer/src/decoder/FFmpegDecoder.cpp new file mode 100644 index 0000000..3d4fd60 --- /dev/null +++ b/libEcliptPlayer/src/decoder/FFmpegDecoder.cpp @@ -0,0 +1,346 @@ +#include "../../include/eclipt/Decoder.h" +#include +#include +#include + +namespace eclipt { + +struct FFmpegDecoder::Impl { + AVCodecContext* codec_ctx = nullptr; + const AVCodec* codec = nullptr; + AVFrame* frame = nullptr; + AVFrame* hw_frame = nullptr; + AVBufferRef* hw_device_ctx = nullptr; + DecodeStats stats; + DecoderConfig config; + StreamMetadata stream_info; + HardwareAPI current_hw_api = HardwareAPI::None; + std::string last_error; + bool is_open = false; + + Impl() { + frame = av_frame_alloc(); + hw_frame = av_frame_alloc(); + } + + ~Impl() { + if (frame) av_frame_free(&frame); + if (hw_frame) av_frame_free(&hw_frame); + if (codec_ctx) avcodec_free_context(&codec_ctx); + if (hw_device_ctx) av_buffer_unref(&hw_device_ctx); + } + + bool openDecoder(const StreamMetadata& stream, const DecoderConfig& cfg) { + config = cfg; + stream_info = stream; + + codec = avcodec_find_decoder_by_name(stream.codec_name.c_str()); + if (!codec) { + codec = avcodec_find_decoder(AV_CODEC_ID_H264); + } + + if (!codec) { + last_error = "Codec not found"; + return false; + } + + codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + last_error = "Failed to allocate codec context"; + return false; + } + + codec_ctx->width = stream.width; + codec_ctx->height = stream.height; + codec_ctx->thread_count = cfg.thread_count > 0 ? cfg.thread_count : 0; + codec_ctx->flags2 |= AV_CODEC_FLAG2_FAST; + + if (cfg.low_latency) { + codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; + } + + if (cfg.use_hw_acceleration) { + if (!setupHardwareAcceleration(cfg.hw_device)) { + last_error = "Hardware acceleration setup failed, falling back to software"; + } + } + + int ret = avcodec_open2(codec_ctx, codec, nullptr); + if (ret < 0) { + char errbuf[128]; + av_strerror(ret, errbuf, sizeof(errbuf)); + last_error = "Failed to open codec: " + std::string(errbuf); + return false; + } + + is_open = true; + stats.is_hardware = (current_hw_api != HardwareAPI::None); + + return true; + } + + bool setupHardwareAcceleration(const std::string& device) { + if (device == "cuda" || device == "auto") { + for (int i = 0; i < FFmpegDecoder::getAvailableHardwareAPIs().size(); ++i) { + auto apis = FFmpegDecoder::getAvailableHardwareAPIs(); + if (i < (int)apis.size()) { + current_hw_api = apis[i]; + return true; + } + } + } + current_hw_api = HardwareAPI::None; + return false; + } + + bool sendPacket(const Packet& packet) { + if (!codec_ctx || !is_open) return false; + + AVPacket* avpkt = av_packet_alloc(); + avpkt->data = const_cast(packet.data.data()); + avpkt->size = static_cast(packet.data.size()); + avpkt->pts = packet.pts; + avpkt->dts = packet.dts; + avpkt->duration = packet.duration; + avpkt->flags = packet.flags; + + int ret = avcodec_send_packet(codec_ctx, avpkt); + av_packet_free(&avpkt); + if (ret < 0) { + return false; + } + return true; + } + + bool receiveFrame(VideoFrame& video_frame) { + if (!codec_ctx || !is_open) return false; + + int ret = avcodec_receive_frame(codec_ctx, frame); + if (ret < 0) { + return false; + } + + video_frame.width = frame->width; + video_frame.height = frame->height; + video_frame.pts = frame->pts; + video_frame.dts = frame->best_effort_timestamp; + video_frame.duration = frame->duration; + + if (frame->format == AV_PIX_FMT_YUV420P) { + video_frame.format = PixelFormat::YUV420; + video_frame.planes[0] = FrameBuffer(frame->data[0], frame->width * frame->height, frame->linesize[0]); + video_frame.planes[1] = FrameBuffer(frame->data[1], frame->width * frame->height / 4, frame->linesize[1]); + video_frame.planes[2] = FrameBuffer(frame->data[2], frame->width * frame->height / 4, frame->linesize[2]); + } else { + video_frame.format = PixelFormat::RGB24; + size_t size = frame->width * frame->height * 3; + video_frame.planes[0] = FrameBuffer(frame->data[0], size, frame->linesize[0]); + } + + stats.decoded_frames++; + return true; + } +}; + +FFmpegDecoder::FFmpegDecoder() : pImpl(std::make_unique()) {} + +FFmpegDecoder::~FFmpegDecoder() = default; + +bool FFmpegDecoder::open(const StreamMetadata& stream, const DecoderConfig& config) { + return pImpl->openDecoder(stream, config); +} + +void FFmpegDecoder::close() { + pImpl->is_open = false; +} + +bool FFmpegDecoder::isOpen() const { + return pImpl->is_open; +} + +bool FFmpegDecoder::sendPacket(const Packet& packet) { + return pImpl->sendPacket(packet); +} + +bool FFmpegDecoder::receiveFrame(VideoFrame& frame) { + return pImpl->receiveFrame(frame); +} + +bool FFmpegDecoder::flush() { + if (!pImpl->codec_ctx) return false; + return avcodec_send_packet(pImpl->codec_ctx, nullptr) >= 0; +} + +DecoderType FFmpegDecoder::getDecoderType() const { + return pImpl->current_hw_api != HardwareAPI::None ? DecoderType::Hardware : DecoderType::Software; +} + +HardwareAPI FFmpegDecoder::getHardwareAPI() const { + return pImpl->current_hw_api; +} + +const DecodeStats& FFmpegDecoder::getStats() const { + return pImpl->stats; +} + +void FFmpegDecoder::setHardwareAPI(HardwareAPI api) { + pImpl->current_hw_api = api; +} + +bool FFmpegDecoder::supportsFormat(const std::string& codec) const { + const AVCodec* c = avcodec_find_decoder_by_name(codec.c_str()); + return c != nullptr; +} + +std::string FFmpegDecoder::getLastError() const { + return pImpl->last_error; +} + +DecoderCapabilities FFmpegDecoder::getCapabilities() { + DecoderCapabilities caps; + caps.supports_hw_decode = true; + caps.max_resolution_width = 7680; + caps.max_resolution_height = 4320; + caps.supports_10bit = true; + caps.supports_hdr = true; + + auto apis = getAvailableHardwareAPIs(); + caps.supported_apis = apis; + + return caps; +} + +std::vector FFmpegDecoder::getAvailableHardwareAPIs() { + std::vector apis; + apis.push_back(HardwareAPI::None); + apis.push_back(HardwareAPI::VAAPI); + apis.push_back(HardwareAPI::VDPAU); + apis.push_back(HardwareAPI::CUDA); + return apis; +} + +HardwareAPI FFmpegDecoder::probeBestHardwareAPI(const std::string&) { + return HardwareAPI::None; +} + +void* FFmpegDecoder::getHardwareDeviceContext() const { + return pImpl->hw_device_ctx; +} + +struct FramePool::Impl { + std::vector available_frames; + std::vector in_use_frames; + size_t max_size; + PixelFormat format; + uint32_t width; + uint32_t height; + std::mutex mutex; + + Impl(size_t max, PixelFormat fmt, uint32_t w, uint32_t h) + : max_size(max), format(fmt), width(w), height(h) {} + + VideoFrame allocateFrame() { + VideoFrame frame; + frame.width = width; + frame.height = height; + frame.format = format; + + size_t size = frame.totalSize(); + uint8_t* buffer = new uint8_t[size]; + + frame.planes[0] = FrameBuffer(buffer, size, width); + + return frame; + } +}; + +FramePool::FramePool(size_t max_size, PixelFormat format, uint32_t width, uint32_t height) + : pImpl(std::make_unique(max_size, format, width, height)) {} + +FramePool::~FramePool() { + clear(); +} + +VideoFrame FramePool::acquire() { + std::lock_guard lock(pImpl->mutex); + + if (!pImpl->available_frames.empty()) { + VideoFrame frame = std::move(pImpl->available_frames.back()); + pImpl->available_frames.pop_back(); + return frame; + } + + return pImpl->allocateFrame(); +} + +void FramePool::release(VideoFrame&& frame) { + std::lock_guard lock(pImpl->mutex); + + if (pImpl->available_frames.size() < pImpl->max_size) { + pImpl->available_frames.push_back(std::move(frame)); + } +} + +void FramePool::clear() { + std::lock_guard lock(pImpl->mutex); + pImpl->available_frames.clear(); + pImpl->in_use_frames.clear(); +} + +size_t FramePool::available() const { + return pImpl->available_frames.size(); +} + +void FramePool::setFormat(PixelFormat format) { + pImpl->format = format; +} + +void FramePool::setDimensions(uint32_t width, uint32_t height) { + pImpl->width = width; + pImpl->height = height; +} + +struct DecoderPool::Impl { + std::vector> decoders; + size_t max_decoders; + std::mutex mutex; + + Impl() : max_decoders(4) {} +}; + +DecoderPool::DecoderPool() : pImpl(std::make_unique()) {} + +DecoderPool::~DecoderPool() = default; + +void DecoderPool::setMaxDecoders(size_t count) { + pImpl->max_decoders = count; +} + +size_t DecoderPool::getMaxDecoders() const { + return pImpl->max_decoders; +} + +std::shared_ptr DecoderPool::acquire(const StreamMetadata& stream, const DecoderConfig& config) { + std::lock_guard lock(pImpl->mutex); + + auto decoder = std::make_shared(); + if (decoder->open(stream, config)) { + return decoder; + } + return nullptr; +} + +void DecoderPool::release(std::shared_ptr) { + std::lock_guard lock(pImpl->mutex); +} + +void DecoderPool::clear() { + std::lock_guard lock(pImpl->mutex); + pImpl->decoders.clear(); +} + +size_t DecoderPool::activeCount() const { + return pImpl->decoders.size(); +} + +} diff --git a/libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp b/libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp new file mode 100644 index 0000000..27263b0 --- /dev/null +++ b/libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp @@ -0,0 +1,203 @@ +#include "../../include/eclipt/Demuxer.h" +#include +#include + +namespace eclipt { + +struct FFmpegDemuxer::Impl { + AVFormatContext* fmt_ctx = nullptr; + AVPacket* pkt = nullptr; + std::vector streams; + int preferred_video_stream = -1; + int preferred_audio_stream = -1; + int preferred_subtitle_stream = -1; + ReadCallback read_callback; + std::string last_error; + + Impl() : pkt(av_packet_alloc()) {} + + ~Impl() { + if (fmt_ctx) { + avformat_close_input(&fmt_ctx); + } + if (pkt) { + av_packet_free(&pkt); + } + } + + bool initFormatContext(const std::string& url) { + int ret = avformat_open_input(&fmt_ctx, url.c_str(), nullptr, nullptr); + if (ret < 0) { + char errbuf[128]; + av_strerror(ret, errbuf, sizeof(errbuf)); + last_error = std::string("Failed to open input: ") + errbuf; + return false; + } + return true; + } + + bool findStreams() { + int ret = avformat_find_stream_info(fmt_ctx, nullptr); + if (ret < 0) { + char errbuf[128]; + av_strerror(ret, errbuf, sizeof(errbuf)); + last_error = std::string("Failed to find stream info: ") + errbuf; + return false; + } + + streams.clear(); + + for (unsigned int i = 0; i < fmt_ctx->nb_streams; ++i) { + AVStream* avs = fmt_ctx->streams[i]; + StreamMetadata sm; + sm.stream_index = i; + + if (avs->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + sm.media_type = MediaType::Video; + sm.width = avs->codecpar->width; + sm.height = avs->codecpar->height; + sm.fps_num = avs->avg_frame_rate.num; + sm.fps_den = avs->avg_frame_rate.den; + } else if (avs->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + sm.media_type = MediaType::Audio; + sm.sample_rate = avs->codecpar->sample_rate; + sm.channels = avs->codecpar->ch_layout.nb_channels; + } else if (avs->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) { + sm.media_type = MediaType::Subtitle; + } else { + sm.media_type = MediaType::Data; + } + + if (avs->codecpar->codec_id != AV_CODEC_ID_NONE) { + sm.codec_name = avcodec_get_name(avs->codecpar->codec_id); + } + + if (avs->metadata) { + AVDictionaryEntry* e = av_dict_get(avs->metadata, "language", nullptr, 0); + if (e) sm.language = e->value; + e = av_dict_get(avs->metadata, "title", nullptr, 0); + if (e) sm.title = e->value; + } + + sm.duration = avs->duration; + sm.bitrate = avs->codecpar->bit_rate; + sm.is_default = (avs->disposition & AV_DISPOSITION_DEFAULT) != 0; + sm.is_forced = (avs->disposition & AV_DISPOSITION_FORCED) != 0; + + streams.push_back(sm); + } + + return true; + } + + const StreamMetadata* findBestStream(AVMediaType type) const { + int idx = av_find_best_stream(fmt_ctx, type, -1, -1, nullptr, 0); + if (idx < 0) return nullptr; + + for (const auto& sm : streams) { + if (sm.stream_index == idx) return &sm; + } + return nullptr; + } + + int64_t getCurrentPositionMs() const { + if (!fmt_ctx || !fmt_ctx->pb) return 0; + return avio_tell(fmt_ctx->pb); + } +}; + +FFmpegDemuxer::FFmpegDemuxer() : pImpl(std::make_unique()) {} +FFmpegDemuxer::~FFmpegDemuxer() = default; + +bool FFmpegDemuxer::open(const std::string& url, const std::string&) { + pImpl->last_error.clear(); + if (!pImpl->initFormatContext(url)) return false; + if (!pImpl->findStreams()) return false; + return true; +} + +void FFmpegDemuxer::close() { + if (pImpl->fmt_ctx) avformat_close_input(&pImpl->fmt_ctx); + pImpl->streams.clear(); +} + +bool FFmpegDemuxer::isOpen() const { return pImpl->fmt_ctx != nullptr; } + +bool FFmpegDemuxer::seek(int64_t timestamp_ms, SeekDirection dir) { + if (!pImpl->fmt_ctx) return false; + int64_t seek_target = timestamp_ms; + if (dir == SeekDirection::Backward) seek_target = pImpl->getCurrentPositionMs() - timestamp_ms; + else if (dir == SeekDirection::Forward) seek_target = pImpl->getCurrentPositionMs() + timestamp_ms; + return av_seek_frame(pImpl->fmt_ctx, -1, seek_target * 1000, AVSEEK_FLAG_BACKWARD) >= 0; +} + +bool FFmpegDemuxer::seekToKeyframe(int64_t timestamp_ms) { + if (!pImpl->fmt_ctx) return false; + return av_seek_frame(pImpl->fmt_ctx, -1, timestamp_ms * 1000, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME) >= 0; +} + +bool FFmpegDemuxer::readPacket(Packet& packet) { + if (!pImpl->fmt_ctx || !pImpl->pkt) return false; + + int ret = av_read_frame(pImpl->fmt_ctx, pImpl->pkt); + if (ret < 0) { + if (ret == AVERROR_EOF) { packet.clear(); return false; } + char errbuf[128]; + av_strerror(ret, errbuf, sizeof(errbuf)); + pImpl->last_error = std::string("Read error: ") + errbuf; + return false; + } + + packet.stream_index = pImpl->pkt->stream_index; + packet.pts = pImpl->pkt->pts; + packet.dts = pImpl->pkt->dts; + packet.duration = pImpl->pkt->duration; + packet.flags = pImpl->pkt->flags; + + if (packet.stream_index >= 0 && packet.stream_index < static_cast(pImpl->streams.size())) { + packet.media_type = pImpl->streams[packet.stream_index].media_type; + } + + packet.data.resize(pImpl->pkt->size); + if (pImpl->pkt->size > 0) { + std::memcpy(packet.data.data(), pImpl->pkt->data, pImpl->pkt->size); + } + + av_packet_unref(pImpl->pkt); + return true; +} + +int FFmpegDemuxer::getPacketCount() const { return 0; } + +std::vector FFmpegDemuxer::getStreams() const { return pImpl->streams; } + +const StreamMetadata* FFmpegDemuxer::getStream(int index) const { + if (index >= 0 && index < static_cast(pImpl->streams.size())) return &pImpl->streams[index]; + return nullptr; +} + +const StreamMetadata* FFmpegDemuxer::getBestVideoStream() const { return pImpl->findBestStream(AVMEDIA_TYPE_VIDEO); } +const StreamMetadata* FFmpegDemuxer::getBestAudioStream() const { return pImpl->findBestStream(AVMEDIA_TYPE_AUDIO); } +const StreamMetadata* FFmpegDemuxer::getBestSubtitleStream() const { return pImpl->findBestStream(AVMEDIA_TYPE_SUBTITLE); } + +int64_t FFmpegDemuxer::getDuration() const { + if (!pImpl->fmt_ctx) return 0; + if (pImpl->fmt_ctx->duration == AV_NOPTS_VALUE) return 0; + return pImpl->fmt_ctx->duration / 1000; +} + +int64_t FFmpegDemuxer::getStartTime() const { return pImpl->fmt_ctx ? pImpl->fmt_ctx->start_time : 0; } +int64_t FFmpegDemuxer::getBitrate() const { return pImpl->fmt_ctx ? pImpl->fmt_ctx->bit_rate : 0; } +std::string FFmpegDemuxer::getUrl() const { return pImpl->fmt_ctx && pImpl->fmt_ctx->url ? pImpl->fmt_ctx->url : ""; } +std::string FFmpegDemuxer::getFormatName() const { return pImpl->fmt_ctx && pImpl->fmt_ctx->iformat && pImpl->fmt_ctx->iformat->name ? pImpl->fmt_ctx->iformat->name : ""; } + +void FFmpegDemuxer::setPreferredStream(int media_type, int stream_index) { + if (media_type == AVMEDIA_TYPE_VIDEO) pImpl->preferred_video_stream = stream_index; + else if (media_type == AVMEDIA_TYPE_AUDIO) pImpl->preferred_audio_stream = stream_index; + else if (media_type == AVMEDIA_TYPE_SUBTITLE) pImpl->preferred_subtitle_stream = stream_index; +} + +void FFmpegDemuxer::setReadCallback(ReadCallback callback) { pImpl->read_callback = std::move(callback); } +void* FFmpegDemuxer::getAVFormatContext() const { return pImpl->fmt_ctx; } + +} diff --git a/libEcliptPlayer/src/playlist/EPG.cpp b/libEcliptPlayer/src/playlist/EPG.cpp new file mode 100644 index 0000000..13ee02f --- /dev/null +++ b/libEcliptPlayer/src/playlist/EPG.cpp @@ -0,0 +1,198 @@ +#include "../../include/eclipt/EPG.h" +#include +#include +#include +#include + +namespace eclipt { + +static int64_t parseXmltvTimeImpl(const std::string& str); + +const EPGEvent* EPGChannel::getCurrentEvent() const { + int64_t now = std::time(nullptr); + for (const auto& event : events) { + if (event.start_time <= now && event.end_time > now) { + return &event; + } + } + return nullptr; +} + +std::vector EPGChannel::getEventsInRange(int64_t start, int64_t end) const { + std::vector result; + for (const auto& event : events) { + if (event.end_time > start && event.start_time < end) { + result.push_back(&event); + } + } + return result; +} + +bool EPGData::loadFromFile(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + + XmltvParser parser; + return parser.parse(buffer.str(), *this); +} + +bool EPGData::loadFromXml(const std::string& xml_content) { + XmltvParser parser; + return parser.parse(xml_content, *this); +} + +bool EPGData::loadFromUrl(const std::string& url) { + return false; +} + +bool EPGData::merge(const EPGData& other) { + for (const auto& channel : other.channels) { + auto it = channels.find(channel.first); + if (it == channels.end()) { + channels[channel.first] = channel.second; + } else { + for (const auto& event : channel.second.events) { + it->second.events.push_back(event); + } + } + } + return true; +} + +const EPGChannel* EPGData::findChannel(const std::string& id) const { + auto it = channels.find(id); + if (it != channels.end()) { + return &it->second; + } + return nullptr; +} + +std::vector EPGData::findChannels(const std::string& group) const { + std::vector result; + for (const auto& channel : channels) { + if (channel.second.group == group) { + result.push_back(&channel.second); + } + } + return result; +} + +bool XmltvParser::parse(const std::string& content, EPGData& epg) { + std::istringstream stream(content); + std::string line; + + EPGChannel* current_channel = nullptr; + + while (std::getline(stream, line)) { + if (line.find("events.push_back(event); + } + } + } + + return !epg.channels.empty(); +} + +bool XmltvParser::parseChannel(const std::string& xml, EPGChannel& channel) { + size_t id_start = xml.find("channel="); + if (id_start == std::string::npos) return false; + + id_start = xml.find('"', id_start) + 1; + size_t id_end = xml.find('"', id_start); + channel.id = xml.substr(id_start, id_end - id_start); + + return true; +} + +bool XmltvParser::parseProgramme(const std::string& xml, EPGEvent& event) { + size_t start_pos = xml.find("start="); + if (start_pos == std::string::npos) return false; + + start_pos = xml.find('"', start_pos) + 1; + size_t start_end = xml.find('"', start_pos); + std::string start_str = xml.substr(start_pos, start_end - start_pos); + + size_t stop_pos = xml.find("stop="); + if (stop_pos == std::string::npos) return false; + + stop_pos = xml.find('"', stop_pos) + 1; + size_t stop_end = xml.find('"', stop_pos); + std::string stop_str = xml.substr(stop_pos, stop_end - stop_pos); + + event.start_time = parseXmltvTimeImpl(start_str); + event.end_time = parseXmltvTimeImpl(stop_str); + event.duration = event.end_time - event.start_time; + + return true; +} + +int64_t parseXmltvTimeImpl(const std::string& str) { + if (str.size() < 14) return 0; + + int year = std::stoi(str.substr(0, 4)); + int month = std::stoi(str.substr(4, 2)); + int day = std::stoi(str.substr(6, 2)); + int hour = std::stoi(str.substr(8, 2)); + int min = std::stoi(str.substr(10, 2)); + int sec = std::stoi(str.substr(12, 2)); + + std::tm tm = {}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + + return std::mktime(&tm); +} + +EPGFetcher::EPGFetcher() = default; + +EPGFetcher::~EPGFetcher() = default; + +void EPGFetcher::setBaseUrl(const std::string& url) { + base_url_ = url; +} + +void EPGFetcher::setAuth(const std::string& username, const std::string& password) { + username_ = username; + password_ = password; +} + +bool EPGFetcher::fetch(EPGData& epg, const std::string& channel_id) { + return false; +} + +bool EPGFetcher::fetchAll(EPGData& epg) { + return false; +} + +void EPGFetcher::cancel() { + cancelled_ = true; +} + +void EPGFetcher::setProgressCallback(ProgressCallback callback) { + progress_callback_ = std::move(callback); +} + +std::string EPGFetcher::buildUrl(const std::string& channel_id) { + return base_url_; +} + +bool EPGFetcher::authenticate() { + return true; +} + +} diff --git a/libEcliptPlayer/src/playlist/Playlist.cpp b/libEcliptPlayer/src/playlist/Playlist.cpp new file mode 100644 index 0000000..18a5e1e --- /dev/null +++ b/libEcliptPlayer/src/playlist/Playlist.cpp @@ -0,0 +1,154 @@ +#include "../../include/eclipt/Playlist.h" +#include +#include +#include + +namespace eclipt { + +bool Playlist::loadFromFile(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + return loadFromString(buffer.str()); +} + +bool Playlist::loadFromString(const std::string& content) { + M3UParser parser; + return parser.parse(content, *this); +} + +bool Playlist::loadFromUrl(const std::string& url) { + return false; +} + +std::vector Playlist::getLiveStreams() const { + std::vector result; + for (const auto& item : items) { + if (item.is_live) { + result.push_back(item); + } + } + return result; +} + +std::vector Playlist::getByGroup(const std::string& group) const { + std::vector result; + for (const auto& item : items) { + if (item.group == group) { + result.push_back(item); + } + } + return result; +} + +PlaylistItem* Playlist::findById(const std::string& id) { + for (auto& item : items) { + if (item.tvg_id == id) { + return &item; + } + } + return nullptr; +} + +bool M3UParser::parse(const std::string& content, Playlist& playlist) { + std::istringstream stream(content); + std::string line; + + bool is_extm3u = false; + + while (std::getline(stream, line)) { + if (line.empty() || line[0] == '#') { + if (line == "#EXTM3U") { + is_extm3u = true; + playlist.type = PlaylistType::M3U8; + } + continue; + } + + PlaylistItem item; + item.url = line; + + if (is_extm3u) { + while (std::getline(stream, line)) { + if (line.empty()) continue; + + if (line.rfind("#EXTINF:", 0) == 0) { + parseExtInf(line, item); + } else if (line.rfind("#EXTVLCOPT:", 0) == 0) { + // Extended options + } else { + item.url = line; + playlist.items.push_back(item); + } + } + } else { + playlist.items.push_back(item); + } + } + + return !playlist.items.empty(); +} + +bool M3UParser::parseExtInf(const std::string& line, PlaylistItem& item) { + size_t comma = line.find(','); + if (comma == std::string::npos) return false; + + std::string attrs = line.substr(8, comma - 8); + + size_t colon = attrs.find(':'); + if (colon != std::string::npos) { + std::string duration_str = attrs.substr(0, colon); + try { + item.duration = std::stoll(duration_str); + } catch (...) {} + } + + item.name = line.substr(comma + 1); + + size_t tvg_name_pos = attrs.find("tvg-name="); + if (tvg_name_pos != std::string::npos) { + size_t start = tvg_name_pos + 9; + size_t end = attrs.find('"', start + 1); + if (end != std::string::npos) { + item.tvg_name = attrs.substr(start + 1, end - start - 2); + } + } + + size_t tvg_id_pos = attrs.find("tvg-id="); + if (tvg_id_pos != std::string::npos) { + size_t start = tvg_id_pos + 7; + size_t end = attrs.find('"', start + 1); + if (end != std::string::npos) { + item.tvg_id = attrs.substr(start + 1, end - start - 2); + } + } + + size_t group_pos = attrs.find("group-title="); + if (group_pos != std::string::npos) { + size_t start = group_pos + 12; + size_t end = attrs.find('"', start + 1); + if (end != std::string::npos) { + item.group = attrs.substr(start + 1, end - start - 2); + } + } + + return true; +} + +bool M3UParser::parseAttribute(const std::string& attr, std::string& key, std::string& value) { + size_t eq = attr.find('='); + if (eq == std::string::npos) return false; + + key = attr.substr(0, eq); + value = attr.substr(eq + 1); + + if (value.front() == '"' && value.back() == '"') { + value = value.substr(1, value.length() - 2); + } + + return true; +} + +} diff --git a/libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp b/libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp new file mode 100644 index 0000000..514f582 --- /dev/null +++ b/libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp @@ -0,0 +1,245 @@ +#include "../../include/eclipt/Subtitle.h" +#include +#include +#include +#include +#include + +namespace eclipt { + +SubtitleDecoder::SubtitleDecoder() = default; +SubtitleDecoder::~SubtitleDecoder() = default; + +bool SubtitleDecoder::open(const std::string& url) { + std::ifstream file(url, std::ios::binary); + if (!file.is_open()) return false; + + std::stringstream buffer; + buffer << file.rdbuf(); + data_.assign(buffer.str().begin(), buffer.str().end()); + + is_open_ = parse(); + return is_open_; +} + +bool SubtitleDecoder::open(const uint8_t* data, size_t size, SubtitleType type) { + data_.assign(data, data + size); + current_type_ = type; + is_open_ = parse(); + return is_open_; +} + +void SubtitleDecoder::close() { + cues_.clear(); + data_.clear(); + is_open_ = false; +} + +bool SubtitleDecoder::parse() { + switch (current_type_) { + case SubtitleType::SRT: return parseSRT(); + case SubtitleType::VTT: return parseVTT(); + case SubtitleType::ASS: + case SubtitleType::SSA: return parseASS(); + default: return false; + } +} + +bool SubtitleDecoder::parseSRT() { + std::string content(data_.begin(), data_.end()); + std::istringstream stream(content); + std::string line; + + while (std::getline(stream, line)) { + if (line.empty()) continue; + + SubtitleCue cue; + cue.start_time = parseTimestamp(line); + + std::getline(stream, line); + std::string end_time_str = line; + + size_t arrow = end_time_str.find("-->"); + if (arrow != std::string::npos) { + std::string end_str = end_time_str.substr(arrow + 3); + cue.end_time = parseTimestamp(end_str); + } + + std::string text; + while (std::getline(stream, line) && !line.empty()) { + if (!text.empty()) text += "\n"; + text += line; + } + cue.text = text; + cue.lines = splitLines(text); + + if (cue.isValid()) { + cues_.push_back(cue); + } + } + + return !cues_.empty(); +} + +bool SubtitleDecoder::parseVTT() { + return parseSRT(); +} + +bool SubtitleDecoder::parseASS() { + std::string content(data_.begin(), data_.end()); + std::istringstream stream(content); + std::string line; + + while (std::getline(stream, line)) { + if (line.substr(0, 9) == "Dialogue:") { + SubtitleCue cue; + + size_t pos = 9; + for (int i = 0; i < 9 && pos < line.size(); ++i) { + size_t comma = line.find(',', pos); + if (comma == std::string::npos) break; + pos = comma + 1; + } + + cue.text = line.substr(pos); + cue.lines = splitLines(cue.text); + + cues_.push_back(cue); + } + } + + return !cues_.empty(); +} + +int64_t SubtitleDecoder::parseTimestamp(const std::string& str) { + size_t colon1 = str.find(':'); + size_t colon2 = str.find(':', colon1 + 1); + size_t comma = str.find(','); + + if (colon2 == std::string::npos || comma == std::string::npos) return 0; + + int hours = std::stoi(str.substr(0, colon1)); + int minutes = std::stoi(str.substr(colon1 + 1, colon2 - colon1 - 1)); + int seconds = std::stoi(str.substr(colon2 + 1, comma - colon2 - 1)); + int millis = std::stoi(str.substr(comma + 1)); + + return (hours * 3600 + minutes * 60 + seconds) * 1000LL + millis; +} + +std::vector SubtitleDecoder::splitLines(const std::string& text) { + std::vector lines; + std::istringstream stream(text); + std::string line; + + while (std::getline(stream, line)) { + lines.push_back(line); + } + + return lines; +} + +const SubtitleCue* SubtitleDecoder::getCueAtTime(int64_t pts) const { + for (const auto& cue : cues_) { + if (cue.isActive(pts)) { + return &cue; + } + } + return nullptr; +} + +struct SubtitleRenderer::Impl { + SubtitleDecoder decoder; + SubtitleStyle style; + int video_width = 1920; + int video_height = 1080; + int fps_num = 30; + int fps_den = 1; + bool initialized = false; + + Impl() { + style.font_name = "Arial"; + style.font_size = 24; + style.primary_color = 0xFFFFFFFF; + style.alignment = 2; + } +}; + +SubtitleRenderer::SubtitleRenderer() : pImpl(std::make_unique()) {} + +SubtitleRenderer::~SubtitleRenderer() = default; + +bool SubtitleRenderer::initialize(int width, int height) { + pImpl->video_width = width; + pImpl->video_height = height; + pImpl->initialized = true; + return true; +} + +void SubtitleRenderer::shutdown() { + pImpl->initialized = false; +} + +bool SubtitleRenderer::loadSubtitles(const std::string& path) { + return pImpl->decoder.open(path); +} + +bool SubtitleRenderer::loadSubtitles(const uint8_t* data, size_t size, SubtitleType type) { + return pImpl->decoder.open(data, size, type); +} + +void SubtitleRenderer::clearSubtitles() { + pImpl->decoder.close(); +} + +void SubtitleRenderer::setStyle(const SubtitleStyle& style) { + pImpl->style = style; +} + +SubtitleStyle SubtitleRenderer::getStyle() const { + return pImpl->style; +} + +RenderedSubtitle SubtitleRenderer::render(int64_t pts) { + RenderedSubtitle result; + result.pts = pts; + + const auto* cue = pImpl->decoder.getCueAtTime(pts); + if (!cue) return result; + + result.duration = cue->end_time - cue->start_time; + + VideoFrame frame; + frame.width = pImpl->video_width; + frame.height = pImpl->video_height; + frame.format = PixelFormat::BGRA32; + + size_t size = frame.width * frame.height * 4; + uint8_t* buffer = new uint8_t[size]; + std::memset(buffer, 0, size); + + frame.planes[0] = FrameBuffer(buffer, size, frame.width * 4); + result.frame = std::move(frame); + + return result; +} + +const SubtitleCue* SubtitleRenderer::getCurrentCue(int64_t pts) const { + return pImpl->decoder.getCueAtTime(pts); +} + +bool SubtitleRenderer::hasSubtitles() const { + return pImpl->decoder.isOpen(); +} + +size_t SubtitleRenderer::getCueCount() const { + return pImpl->decoder.getCues().size(); +} + +void SubtitleRenderer::setVideoParams(int width, int height, int fps_num, int fps_den) { + pImpl->video_width = width; + pImpl->video_height = height; + pImpl->fps_num = fps_num; + pImpl->fps_den = fps_den; +} + +} diff --git a/platform/linux/include/eclipt-linux/EcliptLinux.h b/platform/linux/include/eclipt-linux/EcliptLinux.h new file mode 100644 index 0000000..03ba181 --- /dev/null +++ b/platform/linux/include/eclipt-linux/EcliptLinux.h @@ -0,0 +1,110 @@ +#ifndef ECLIPT_LINUX_PLAYER_H +#define ECLIPT_LINUX_PLAYER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eclipt { + class EcliptPlayer; + struct VideoFrame; + struct AudioFrame; +} + +namespace eclipt { +namespace platform { +namespace linux { + +enum class DisplayBackend { + SDL2, + DRM, + X11, + Wayland +}; + +struct LinuxDisplayConfig { + DisplayBackend backend = DisplayBackend::SDL2; + int window_width = 1280; + int window_height = 720; + bool fullscreen = false; + bool vsync = true; + std::string title = "Eclipt"; +}; + +struct LinuxAudioConfig { + int sample_rate = 48000; + int channels = 2; + int buffer_size = 1024; +}; + +class LinuxTexture { +public: + virtual ~LinuxTexture() = default; + virtual bool update(const eclipt::VideoFrame& frame) = 0; + virtual int getWidth() const = 0; + virtual int getHeight() const = 0; +}; + +class LinuxPlayer { +public: + LinuxPlayer(); + ~LinuxPlayer(); + + bool initialize(const LinuxDisplayConfig& display, const LinuxAudioConfig& audio); + void shutdown(); + + bool open(const std::string& url); + void close(); + + bool play(); + bool pause(); + bool stop(); + + bool seek(int64_t position_ms); + + int getState() const; + + void setVolume(float volume); + float getVolume() const; + + void render(); + + using EventCallback = std::function; + void setEventCallback(EventCallback callback); + + eclipt::EcliptPlayer* getCorePlayer(); + +private: + std::unique_ptr core_player_; + + LinuxDisplayConfig display_config_; + LinuxAudioConfig audio_config_; + + bool initialized_ = false; + bool running_ = false; + + std::thread render_thread_; + std::mutex render_mutex_; + std::condition_variable render_cv_; + + EventCallback event_callback_; + + std::vector frame_queue_; + std::mutex frame_mutex_; + std::condition_variable frame_cv_; + + void onVideoFrame(eclipt::VideoFrame&& frame); + void onAudioFrame(eclipt::AudioFrame&& frame); + void renderLoop(); +}; + +} +} +} + +#endif diff --git a/platform/linux/src/EcliptLinux.cpp b/platform/linux/src/EcliptLinux.cpp new file mode 100644 index 0000000..e02c45e --- /dev/null +++ b/platform/linux/src/EcliptLinux.cpp @@ -0,0 +1,145 @@ +#include "../include/eclipt-linux/EcliptLinux.h" +#include "../../../libEcliptPlayer/include/eclipt/EcliptPlayer.h" +#include "../../../libEcliptPlayer/include/eclipt/Frame.h" +#include "../../../libEcliptPlayer/include/eclipt/Config.h" +#include +#include + +namespace eclipt { +namespace platform { +namespace linux { + +LinuxPlayer::LinuxPlayer() = default; + +LinuxPlayer::~LinuxPlayer() { + shutdown(); +} + +bool LinuxPlayer::initialize(const LinuxDisplayConfig& display, const LinuxAudioConfig& audio) { + display_config_ = display; + audio_config_ = audio; + + core_player_ = std::make_unique(); + + core_player_->setVideoCallback([this](VideoFrame&& frame) { + onVideoFrame(std::move(frame)); + }); + + core_player_->setAudioCallback([this](AudioFrame&& frame) { + onAudioFrame(std::move(frame)); + }); + + initialized_ = true; + return true; +} + +void LinuxPlayer::shutdown() { + running_ = false; + + if (render_thread_.joinable()) { + render_cv_.notify_all(); + render_thread_.join(); + } + + if (core_player_) { + core_player_->close(); + } + + initialized_ = false; +} + +bool LinuxPlayer::open(const std::string& url) { + if (!core_player_) return false; + return core_player_->open(url); +} + +void LinuxPlayer::close() { + if (core_player_) { + core_player_->close(); + } +} + +bool LinuxPlayer::play() { + if (!core_player_) return false; + running_ = true; + render_thread_ = std::thread([this]() { renderLoop(); }); + return core_player_->play(); +} + +bool LinuxPlayer::pause() { + if (!core_player_) return false; + return core_player_->pause(); +} + +bool LinuxPlayer::stop() { + running_ = false; + if (core_player_) { + core_player_->stop(); + } + return true; +} + +bool LinuxPlayer::seek(int64_t position_ms) { + if (!core_player_) return false; + return core_player_->seek(position_ms, eclipt::SeekDirection::Absolute); +} + +int LinuxPlayer::getState() const { + if (!core_player_) return 0; + return static_cast(core_player_->getState()); +} + +void LinuxPlayer::setVolume(float volume) { + if (core_player_) { + core_player_->setVolume(volume); + } +} + +float LinuxPlayer::getVolume() const { + if (!core_player_) return 0.0f; + return core_player_->getVolume(); +} + +void LinuxPlayer::render() { + if (!running_) return; + + VideoFrame frame; + { + std::lock_guard lock(frame_mutex_); + if (!frame_queue_.empty()) { + frame = std::move(frame_queue_.front()); + frame_queue_.erase(frame_queue_.begin()); + } + } + + if (frame.isValid()) { + // Render frame using the configured display backend + } +} + +void LinuxPlayer::setEventCallback(EventCallback callback) { + event_callback_ = std::move(callback); +} + +void LinuxPlayer::onVideoFrame(VideoFrame&& frame) { + std::lock_guard lock(frame_mutex_); + if (frame_queue_.size() < 3) { + frame_queue_.push_back(std::move(frame)); + frame_cv_.notify_one(); + } +} + +void LinuxPlayer::onAudioFrame(AudioFrame&& frame) { + // Handle audio frame +} + +void LinuxPlayer::renderLoop() { + while (running_) { + render(); + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } +} + +} +} +} diff --git a/platform/linux/src/main.cpp b/platform/linux/src/main.cpp new file mode 100644 index 0000000..425c205 --- /dev/null +++ b/platform/linux/src/main.cpp @@ -0,0 +1,44 @@ +#include "eclipt-linux/EcliptLinux.h" +#include + +using namespace eclipt::platform::linux; + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cout << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + LinuxDisplayConfig display; + display.title = "Eclipt IPTV Player"; + display.window_width = 1280; + display.window_height = 720; + + LinuxAudioConfig audio; + + LinuxPlayer player; + + if (!player.initialize(display, audio)) { + std::cerr << "Failed to initialize player" << std::endl; + return 1; + } + + if (!player.open(argv[1])) { + std::cerr << "Failed to open: " << argv[1] << std::endl; + return 1; + } + + if (!player.play()) { + std::cerr << "Failed to start playback" << std::endl; + return 1; + } + + std::cout << "Playing: " << argv[1] << std::endl; + std::cout << "Press Enter to stop..." << std::endl; + std::cin.get(); + + player.stop(); + player.shutdown(); + + return 0; +}