This commit is contained in:
2026-03-03 08:13:16 +03:00
commit 85685e7e17
19 changed files with 2887 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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) {
}
}

View 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();
}
}

View 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; }
}

View 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;
}
}

View 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;
}
}

View 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;
}
}