init
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.cache
|
||||||
|
build
|
||||||
100
CMakeLists.txt
Normal file
100
CMakeLists.txt
Normal file
@@ -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}")
|
||||||
61
libEcliptPlayer/include/eclipt/Config.h
Normal file
61
libEcliptPlayer/include/eclipt/Config.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#ifndef ECLIPT_CONFIG_H
|
||||||
|
#define ECLIPT_CONFIG_H
|
||||||
|
|
||||||
|
#include "Frame.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
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<void(LogLevel, const char*)>;
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
156
libEcliptPlayer/include/eclipt/Decoder.h
Normal file
156
libEcliptPlayer/include/eclipt/Decoder.h
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#ifndef ECLIPT_DECODER_H
|
||||||
|
#define ECLIPT_DECODER_H
|
||||||
|
|
||||||
|
#include "Frame.h"
|
||||||
|
#include "Demuxer.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/hwcontext.h>
|
||||||
|
#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<HardwareAPI> supported_apis;
|
||||||
|
std::vector<std::string> 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<HardwareAPI> getAvailableHardwareAPIs();
|
||||||
|
static HardwareAPI probeBestHardwareAPI(const std::string& codec);
|
||||||
|
|
||||||
|
void* getHardwareDeviceContext() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> 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<Impl> pImpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DecoderPool {
|
||||||
|
public:
|
||||||
|
DecoderPool();
|
||||||
|
~DecoderPool();
|
||||||
|
|
||||||
|
void setMaxDecoders(size_t count);
|
||||||
|
size_t getMaxDecoders() const;
|
||||||
|
|
||||||
|
std::shared_ptr<FFmpegDecoder> acquire(const StreamMetadata& stream, const DecoderConfig& config);
|
||||||
|
void release(std::shared_ptr<FFmpegDecoder> decoder);
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
size_t activeCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> pImpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
149
libEcliptPlayer/include/eclipt/Demuxer.h
Normal file
149
libEcliptPlayer/include/eclipt/Demuxer.h
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#ifndef ECLIPT_DEMUXER_H
|
||||||
|
#define ECLIPT_DEMUXER_H
|
||||||
|
|
||||||
|
#include "Frame.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#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<uint8_t> 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<StreamMetadata> 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<size_t(uint8_t*, size_t)>;
|
||||||
|
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<StreamMetadata> 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<Impl> pImpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
104
libEcliptPlayer/include/eclipt/EPG.h
Normal file
104
libEcliptPlayer/include/eclipt/EPG.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#ifndef ECLIPT_EPG_H
|
||||||
|
#define ECLIPT_EPG_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
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<EPGEvent> events;
|
||||||
|
|
||||||
|
const EPGEvent* getCurrentEvent() const;
|
||||||
|
std::vector<const EPGEvent*> 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<std::string, EPGChannel> 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<const EPGChannel*> 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(int percent, const char* status)>;
|
||||||
|
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
|
||||||
102
libEcliptPlayer/include/eclipt/EcliptPlayer.h
Normal file
102
libEcliptPlayer/include/eclipt/EcliptPlayer.h
Normal file
@@ -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 <memory>
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
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<IPlatformHooks> 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<StreamInfo> getAudioTracks() const;
|
||||||
|
std::vector<StreamInfo> 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<Impl> pImpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
90
libEcliptPlayer/include/eclipt/Frame.h
Normal file
90
libEcliptPlayer/include/eclipt/Frame.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#ifndef ECLIPT_FRAME_H
|
||||||
|
#define ECLIPT_FRAME_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
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<void(VideoFrame&&)>;
|
||||||
|
using AudioCallback = std::function<void(AudioFrame&&)>;
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
78
libEcliptPlayer/include/eclipt/Playlist.h
Normal file
78
libEcliptPlayer/include/eclipt/Playlist.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#ifndef ECLIPT_PLAYLIST_H
|
||||||
|
#define ECLIPT_PLAYLIST_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
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<StreamInfo> streams;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Playlist {
|
||||||
|
std::string name;
|
||||||
|
PlaylistType type = PlaylistType::M3U;
|
||||||
|
std::vector<PlaylistItem> 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<PlaylistItem> getLiveStreams() const;
|
||||||
|
std::vector<PlaylistItem> 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
|
||||||
145
libEcliptPlayer/include/eclipt/Subtitle.h
Normal file
145
libEcliptPlayer/include/eclipt/Subtitle.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#ifndef ECLIPT_SUBTITLE_H
|
||||||
|
#define ECLIPT_SUBTITLE_H
|
||||||
|
|
||||||
|
#include "Frame.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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<std::string> 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<SubtitleCue> 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<SubtitleCue> cues_;
|
||||||
|
std::vector<uint8_t> data_;
|
||||||
|
|
||||||
|
bool parseSRT();
|
||||||
|
bool parseVTT();
|
||||||
|
bool parseASS();
|
||||||
|
|
||||||
|
int64_t parseTimestamp(const std::string& str);
|
||||||
|
std::vector<std::string> 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<Impl> pImpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
||||||
455
libEcliptPlayer/src/core/EcliptPlayer.cpp
Normal file
455
libEcliptPlayer/src/core/EcliptPlayer.cpp
Normal file
@@ -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 <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace eclipt {
|
||||||
|
|
||||||
|
struct EcliptPlayer::Impl {
|
||||||
|
PlayerConfig config;
|
||||||
|
std::unique_ptr<IPlatformHooks> platform_hooks;
|
||||||
|
|
||||||
|
std::unique_ptr<IDemuxer> demuxer;
|
||||||
|
std::unique_ptr<IDecoder> video_decoder;
|
||||||
|
std::unique_ptr<ISubtitleRenderer> subtitle_renderer;
|
||||||
|
std::unique_ptr<IFramePool> frame_pool;
|
||||||
|
|
||||||
|
FrameCallback video_callback;
|
||||||
|
AudioCallback audio_callback;
|
||||||
|
LogCallback log_callback;
|
||||||
|
|
||||||
|
std::atomic<PlayerState> state{PlayerState::Stopped};
|
||||||
|
std::atomic<bool> 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> packet_queue;
|
||||||
|
bool eof_reached = false;
|
||||||
|
|
||||||
|
std::mutex frame_queue_mutex;
|
||||||
|
std::condition_variable frame_queue_cv;
|
||||||
|
std::vector<VideoFrame> 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<FFmpegDemuxer>();
|
||||||
|
video_decoder = std::make_unique<FFmpegDecoder>();
|
||||||
|
subtitle_renderer = std::make_unique<SubtitleRenderer>();
|
||||||
|
|
||||||
|
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<FramePool>(
|
||||||
|
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<std::mutex> lock(packet_queue_mutex);
|
||||||
|
packet_queue.clear();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<int64_t>((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<uint8_t>((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<Impl>()) {}
|
||||||
|
|
||||||
|
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<IPlatformHooks> 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<std::mutex> lock(pImpl->frame_queue_mutex);
|
||||||
|
pImpl->decoded_frames.clear();
|
||||||
|
std::lock_guard<std::mutex> 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<StreamInfo> EcliptPlayer::getAudioTracks() const {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<StreamInfo> 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<int64_t>((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<uint8_t>((av + bv) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.planes[0] = FrameBuffer(buffer, size, a.planes[0].stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoFrame EcliptPlayer::getDecodedFrame() {
|
||||||
|
std::lock_guard<std::mutex> 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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
346
libEcliptPlayer/src/decoder/FFmpegDecoder.cpp
Normal file
346
libEcliptPlayer/src/decoder/FFmpegDecoder.cpp
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
#include "../../include/eclipt/Decoder.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
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<uint8_t*>(packet.data.data());
|
||||||
|
avpkt->size = static_cast<int>(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<Impl>()) {}
|
||||||
|
|
||||||
|
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<HardwareAPI> FFmpegDecoder::getAvailableHardwareAPIs() {
|
||||||
|
std::vector<HardwareAPI> 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<VideoFrame> available_frames;
|
||||||
|
std::vector<VideoFrame> 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<Impl>(max_size, format, width, height)) {}
|
||||||
|
|
||||||
|
FramePool::~FramePool() {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoFrame FramePool::acquire() {
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::shared_ptr<FFmpegDecoder>> decoders;
|
||||||
|
size_t max_decoders;
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
|
Impl() : max_decoders(4) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
DecoderPool::DecoderPool() : pImpl(std::make_unique<Impl>()) {}
|
||||||
|
|
||||||
|
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<FFmpegDecoder> DecoderPool::acquire(const StreamMetadata& stream, const DecoderConfig& config) {
|
||||||
|
std::lock_guard<std::mutex> lock(pImpl->mutex);
|
||||||
|
|
||||||
|
auto decoder = std::make_shared<FFmpegDecoder>();
|
||||||
|
if (decoder->open(stream, config)) {
|
||||||
|
return decoder;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DecoderPool::release(std::shared_ptr<FFmpegDecoder>) {
|
||||||
|
std::lock_guard<std::mutex> lock(pImpl->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DecoderPool::clear() {
|
||||||
|
std::lock_guard<std::mutex> lock(pImpl->mutex);
|
||||||
|
pImpl->decoders.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t DecoderPool::activeCount() const {
|
||||||
|
return pImpl->decoders.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
203
libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp
Normal file
203
libEcliptPlayer/src/demuxer/FFmpegDemuxer.cpp
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
#include "../../include/eclipt/Demuxer.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace eclipt {
|
||||||
|
|
||||||
|
struct FFmpegDemuxer::Impl {
|
||||||
|
AVFormatContext* fmt_ctx = nullptr;
|
||||||
|
AVPacket* pkt = nullptr;
|
||||||
|
std::vector<StreamMetadata> 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<Impl>()) {}
|
||||||
|
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<int>(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<StreamMetadata> FFmpegDemuxer::getStreams() const { return pImpl->streams; }
|
||||||
|
|
||||||
|
const StreamMetadata* FFmpegDemuxer::getStream(int index) const {
|
||||||
|
if (index >= 0 && index < static_cast<int>(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; }
|
||||||
|
|
||||||
|
}
|
||||||
198
libEcliptPlayer/src/playlist/EPG.cpp
Normal file
198
libEcliptPlayer/src/playlist/EPG.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#include "../../include/eclipt/EPG.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
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<const EPGEvent*> EPGChannel::getEventsInRange(int64_t start, int64_t end) const {
|
||||||
|
std::vector<const EPGEvent*> 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<const EPGChannel*> EPGData::findChannels(const std::string& group) const {
|
||||||
|
std::vector<const EPGChannel*> 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("<channel") != std::string::npos) {
|
||||||
|
EPGChannel channel;
|
||||||
|
if (parseChannel(line, channel)) {
|
||||||
|
epg.channels[channel.id] = channel;
|
||||||
|
current_channel = &epg.channels[channel.id];
|
||||||
|
}
|
||||||
|
} else if (line.find("<programme") != std::string::npos && current_channel) {
|
||||||
|
EPGEvent event;
|
||||||
|
if (parseProgramme(line, event)) {
|
||||||
|
current_channel->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
154
libEcliptPlayer/src/playlist/Playlist.cpp
Normal file
154
libEcliptPlayer/src/playlist/Playlist.cpp
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#include "../../include/eclipt/Playlist.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<PlaylistItem> Playlist::getLiveStreams() const {
|
||||||
|
std::vector<PlaylistItem> result;
|
||||||
|
for (const auto& item : items) {
|
||||||
|
if (item.is_live) {
|
||||||
|
result.push_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PlaylistItem> Playlist::getByGroup(const std::string& group) const {
|
||||||
|
std::vector<PlaylistItem> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
245
libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp
Normal file
245
libEcliptPlayer/src/subtitle/SubtitleRenderer.cpp
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
#include "../../include/eclipt/Subtitle.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<std::string> SubtitleDecoder::splitLines(const std::string& text) {
|
||||||
|
std::vector<std::string> 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<Impl>()) {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
110
platform/linux/include/eclipt-linux/EcliptLinux.h
Normal file
110
platform/linux/include/eclipt-linux/EcliptLinux.h
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#ifndef ECLIPT_LINUX_PLAYER_H
|
||||||
|
#define ECLIPT_LINUX_PLAYER_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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(const void* event)>;
|
||||||
|
void setEventCallback(EventCallback callback);
|
||||||
|
|
||||||
|
eclipt::EcliptPlayer* getCorePlayer();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<eclipt::EcliptPlayer> 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<eclipt::VideoFrame> frame_queue_;
|
||||||
|
std::mutex frame_mutex_;
|
||||||
|
std::condition_variable frame_cv_;
|
||||||
|
|
||||||
|
void onVideoFrame(eclipt::VideoFrame&& frame);
|
||||||
|
void onAudioFrame(eclipt::AudioFrame&& frame);
|
||||||
|
void renderLoop();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
145
platform/linux/src/EcliptLinux.cpp
Normal file
145
platform/linux/src/EcliptLinux.cpp
Normal file
@@ -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 <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<eclipt::EcliptPlayer>();
|
||||||
|
|
||||||
|
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<int>(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<std::mutex> 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<std::mutex> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
platform/linux/src/main.cpp
Normal file
44
platform/linux/src/main.cpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#include "eclipt-linux/EcliptLinux.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace eclipt::platform::linux;
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
if (argc < 2) {
|
||||||
|
std::cout << "Usage: " << argv[0] << " <url>" << 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user