mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-05 23:40:02 +01:00
File path in mix session file add a relative reference to the location of the session mix file. If SessionCreator cannot find the absolute path, it tries to load the file at the relative location. Done for MediaSource, SessionFileSource and SequenceSource.
1270 lines
38 KiB
C++
1270 lines
38 KiB
C++
#include <thread>
|
||
|
||
using namespace std;
|
||
|
||
// Desktop OpenGL function loader
|
||
#include <glad/glad.h>
|
||
|
||
|
||
// vmix
|
||
#include "defines.h"
|
||
#include "Log.h"
|
||
#include "Resource.h"
|
||
#include "Visitor.h"
|
||
#include "SystemToolkit.h"
|
||
#include "BaseToolkit.h"
|
||
#include "GstToolkit.h"
|
||
#include "RenderingManager.h"
|
||
|
||
#include "MediaPlayer.h"
|
||
|
||
#ifndef NDEBUG
|
||
#define MEDIA_PLAYER_DEBUG
|
||
#endif
|
||
|
||
std::list<MediaPlayer*> MediaPlayer::registered_;
|
||
|
||
MediaPlayer::MediaPlayer()
|
||
{
|
||
// create unique id
|
||
id_ = BaseToolkit::uniqueId();
|
||
|
||
uri_ = "undefined";
|
||
pipeline_ = nullptr;
|
||
opened_ = false;
|
||
enabled_ = true;
|
||
desired_state_ = GST_STATE_PAUSED;
|
||
|
||
failed_ = false;
|
||
seeking_ = false;
|
||
force_software_decoding_ = false;
|
||
decoder_name_ = "";
|
||
rate_ = 1.0;
|
||
position_ = GST_CLOCK_TIME_NONE;
|
||
loop_ = LoopMode::LOOP_REWIND;
|
||
|
||
// start index in frame_ stack
|
||
write_index_ = 0;
|
||
last_index_ = 0;
|
||
|
||
// no PBO by default
|
||
pbo_[0] = pbo_[1] = 0;
|
||
pbo_size_ = 0;
|
||
pbo_index_ = 0;
|
||
pbo_next_index_ = 0;
|
||
|
||
// OpenGL texture
|
||
textureindex_ = 0;
|
||
}
|
||
|
||
MediaPlayer::~MediaPlayer()
|
||
{
|
||
close();
|
||
|
||
// cleanup opengl texture
|
||
if (textureindex_)
|
||
glDeleteTextures(1, &textureindex_);
|
||
|
||
// cleanup picture buffer
|
||
if (pbo_[0])
|
||
glDeleteBuffers(2, pbo_);
|
||
}
|
||
|
||
void MediaPlayer::accept(Visitor& v) {
|
||
v.visit(*this);
|
||
}
|
||
|
||
guint MediaPlayer::texture() const
|
||
{
|
||
if (textureindex_ == 0)
|
||
return Resource::getTextureBlack();
|
||
|
||
return textureindex_;
|
||
}
|
||
|
||
#define LIMIT_DISCOVERER
|
||
|
||
MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
|
||
{
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
Log::Info("Checking file '%s'", uri.c_str());
|
||
#endif
|
||
|
||
#ifdef LIMIT_DISCOVERER
|
||
// Limiting the number of discoverer thread to TWO in parallel
|
||
// Otherwise, a large number of discoverers are executed (when loading a file)
|
||
// leading to a peak of memory and CPU usage : this causes slow down of FPS
|
||
// and a hungry consumption of RAM.
|
||
static std::mutex mtx_primary;
|
||
static std::mutex mtx_secondary;
|
||
bool use_primary = true;
|
||
if ( !mtx_primary.try_lock() ) { // non-blocking
|
||
use_primary = false;
|
||
mtx_secondary.lock(); // blocking
|
||
}
|
||
#endif
|
||
MediaInfo video_stream_info;
|
||
GError *err = NULL;
|
||
GstDiscoverer *discoverer = gst_discoverer_new (15 * GST_SECOND, &err);
|
||
|
||
/* Instantiate the Discoverer */
|
||
if (!discoverer) {
|
||
Log::Warning("MediaPlayer Error creating discoverer instance: %s\n", err->message);
|
||
}
|
||
else {
|
||
GstDiscovererInfo *info = NULL;
|
||
info = gst_discoverer_discover_uri (discoverer, uri.c_str(), &err);
|
||
GstDiscovererResult result = gst_discoverer_info_get_result (info);
|
||
switch (result) {
|
||
case GST_DISCOVERER_URI_INVALID:
|
||
Log::Warning("'%s': Invalid URI", uri.c_str());
|
||
break;
|
||
case GST_DISCOVERER_ERROR:
|
||
Log::Warning("'%s': %s", uri.c_str(), err->message);
|
||
break;
|
||
case GST_DISCOVERER_TIMEOUT:
|
||
Log::Warning("'%s': Timeout loading", uri.c_str());
|
||
break;
|
||
case GST_DISCOVERER_BUSY:
|
||
Log::Warning("'%s': Busy", uri.c_str());
|
||
break;
|
||
case GST_DISCOVERER_MISSING_PLUGINS:
|
||
{
|
||
const GstStructure *s = gst_discoverer_info_get_misc (info);
|
||
gchar *str = gst_structure_to_string (s);
|
||
Log::Warning("'%s': Unknown file format (%s)", uri.c_str(), str);
|
||
g_free (str);
|
||
}
|
||
break;
|
||
default:
|
||
case GST_DISCOVERER_OK:
|
||
break;
|
||
}
|
||
// no error, handle information found
|
||
if ( result == GST_DISCOVERER_OK ) {
|
||
|
||
GList *streams = gst_discoverer_info_get_video_streams(info);
|
||
GList *tmp;
|
||
for (tmp = streams; tmp && !video_stream_info.valid; tmp = tmp->next ) {
|
||
GstDiscovererStreamInfo *tmpinf = (GstDiscovererStreamInfo *) tmp->data;
|
||
if ( GST_IS_DISCOVERER_VIDEO_INFO(tmpinf) )
|
||
{
|
||
// found a video / image stream : fill-in information
|
||
GstDiscovererVideoInfo* vinfo = GST_DISCOVERER_VIDEO_INFO(tmpinf);
|
||
video_stream_info.width = gst_discoverer_video_info_get_width(vinfo);
|
||
video_stream_info.height = gst_discoverer_video_info_get_height(vinfo);
|
||
guint parn = gst_discoverer_video_info_get_par_num(vinfo);
|
||
guint pard = gst_discoverer_video_info_get_par_denom(vinfo);
|
||
video_stream_info.par_width = (video_stream_info.width * parn) / pard;
|
||
video_stream_info.interlaced = gst_discoverer_video_info_is_interlaced(vinfo);
|
||
video_stream_info.bitrate = gst_discoverer_video_info_get_bitrate(vinfo);
|
||
video_stream_info.isimage = gst_discoverer_video_info_is_image(vinfo);
|
||
// if its a video, set duration, framerate, etc.
|
||
if ( !video_stream_info.isimage ) {
|
||
video_stream_info.end = gst_discoverer_info_get_duration (info) ;
|
||
video_stream_info.seekable = gst_discoverer_info_get_seekable (info);
|
||
video_stream_info.framerate_n = gst_discoverer_video_info_get_framerate_num(vinfo);
|
||
video_stream_info.framerate_d = gst_discoverer_video_info_get_framerate_denom(vinfo);
|
||
if (video_stream_info.framerate_n == 0 || video_stream_info.framerate_d == 0) {
|
||
Log::Info("'%s': No framerate indicated in the file; using default 30fps", uri.c_str());
|
||
video_stream_info.framerate_n = 30;
|
||
video_stream_info.framerate_d = 1;
|
||
}
|
||
video_stream_info.dt = ( (GST_SECOND * static_cast<guint64>(video_stream_info.framerate_d)) / (static_cast<guint64>(video_stream_info.framerate_n)) );
|
||
// confirm (or infirm) that its not a single frame
|
||
if ( video_stream_info.end < video_stream_info.dt * 2)
|
||
video_stream_info.isimage = true;
|
||
}
|
||
// try to fill-in the codec information
|
||
GstCaps *caps = gst_discoverer_stream_info_get_caps (tmpinf);
|
||
if (caps) {
|
||
gchar *codecstring = gst_pb_utils_get_codec_description(caps);
|
||
video_stream_info.codec_name = std::string( codecstring );
|
||
g_free(codecstring);
|
||
gst_caps_unref (caps);
|
||
}
|
||
const GstTagList *tags = gst_discoverer_stream_info_get_tags(tmpinf);
|
||
if ( tags ) {
|
||
gchar *container = NULL;
|
||
if ( gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container) )
|
||
video_stream_info.codec_name += ", " + std::string(container);
|
||
if (container)
|
||
g_free(container);
|
||
}
|
||
// exit loop
|
||
// inform that it succeeded
|
||
video_stream_info.valid = true;
|
||
}
|
||
}
|
||
gst_discoverer_stream_info_list_free(streams);
|
||
|
||
if (!video_stream_info.valid) {
|
||
Log::Warning("'%s': No video stream", uri.c_str());
|
||
}
|
||
}
|
||
|
||
if (info)
|
||
gst_discoverer_info_unref (info);
|
||
|
||
g_object_unref( discoverer );
|
||
}
|
||
|
||
g_clear_error (&err);
|
||
|
||
#ifdef LIMIT_DISCOVERER
|
||
if (use_primary)
|
||
mtx_primary.unlock();
|
||
else
|
||
mtx_secondary.unlock();
|
||
#endif
|
||
// return the info
|
||
return video_stream_info;
|
||
}
|
||
|
||
void MediaPlayer::open (const std::string & filename, const string &uri)
|
||
{
|
||
// set path
|
||
filename_ = BaseToolkit::transliterate( filename );
|
||
|
||
// set uri to open
|
||
if (uri.empty())
|
||
uri_ = GstToolkit::filename_to_uri( filename );
|
||
else
|
||
uri_ = uri;
|
||
|
||
if (uri_.empty())
|
||
failed_ = true;
|
||
|
||
// close before re-openning
|
||
if (isOpen())
|
||
close();
|
||
|
||
// start URI discovering thread:
|
||
discoverer_ = std::async( MediaPlayer::UriDiscoverer, uri_);
|
||
// wait for discoverer to finish in the future (test in update)
|
||
|
||
// // debug without thread
|
||
// media_ = MediaPlayer::UriDiscoverer(uri_);
|
||
// if (media_.valid) {
|
||
// timeline_.setEnd( media_.end );
|
||
// timeline_.setStep( media_.dt );
|
||
// execute_open();
|
||
// }
|
||
|
||
}
|
||
|
||
|
||
void MediaPlayer::reopen()
|
||
{
|
||
// re-openning is meaningfull only if it was already open
|
||
if (pipeline_ != nullptr) {
|
||
// reload : terminate pipeline and re-create it
|
||
close();
|
||
execute_open();
|
||
}
|
||
}
|
||
|
||
void MediaPlayer::execute_open()
|
||
{
|
||
// Create gstreamer pipeline :
|
||
// "uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! appsink "
|
||
// equivalent to command line
|
||
// "gst-launch-1.0 uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! ximagesink"
|
||
string description = "uridecodebin name=decoder uri=" + uri_ + " ! queue max-size-time=0 ! ";
|
||
// NB: queue adds some control over the buffer, thereby limiting the frame delay. zero size means no buffering
|
||
|
||
// string description = "uridecodebin name=decoder uri=" + uri_ + " decoder. ! ";
|
||
// description += "audioconvert ! autoaudiosink decoder. ! ";
|
||
|
||
// video deinterlacing method (if media is interlaced)
|
||
// tomsmocomp (0) – Motion Adaptive: Motion Search
|
||
// greedyh (1) – Motion Adaptive: Advanced Detection
|
||
// greedyl (2) – Motion Adaptive: Simple Detection
|
||
// vfir (3) – Blur Vertical
|
||
// linear (4) – Linear
|
||
// scalerbob (6) – Double lines
|
||
if (media_.interlaced)
|
||
description += "deinterlace method=2 ! ";
|
||
|
||
// video convertion algorithm (should only do colorspace conversion, no scaling)
|
||
// chroma-resampler:
|
||
// Duplicates the samples when upsampling and drops when downsampling 0
|
||
// Uses linear interpolation 1 (default)
|
||
// Uses cubic interpolation 2
|
||
// Uses sinc interpolation 3
|
||
// dither:
|
||
// no dithering 0
|
||
// propagate rounding errors downwards 1
|
||
// Dither with floyd-steinberg error diffusion 2
|
||
// Dither with Sierra Lite error diffusion 3
|
||
// ordered dither using a bayer pattern 4 (default)
|
||
description += "videoconvert chroma-resampler=1 dither=0 ! "; // fast
|
||
|
||
// hack to compensate for lack of PTS in gif animations
|
||
if (media_.codec_name.compare("image/gst-libav-gif") == 0){
|
||
description += "videorate ! video/x-raw,framerate=";
|
||
description += std::to_string(media_.framerate_n) + "/";
|
||
description += std::to_string(media_.framerate_d) + " ! ";
|
||
}
|
||
|
||
// set app sink
|
||
description += "appsink name=sink";
|
||
|
||
// parse pipeline descriptor
|
||
GError *error = NULL;
|
||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||
if (error != NULL) {
|
||
Log::Warning("MediaPlayer %s Could not construct pipeline %s:\n%s", std::to_string(id_).c_str(), description.c_str(), error->message);
|
||
g_clear_error (&error);
|
||
failed_ = true;
|
||
return;
|
||
}
|
||
// setup pipeline
|
||
g_object_set(G_OBJECT(pipeline_), "name", std::to_string(id_).c_str(), NULL);
|
||
gst_pipeline_set_auto_flush_bus( GST_PIPELINE(pipeline_), true);
|
||
|
||
// GstCaps *caps = gst_static_caps_get (&frame_render_caps);
|
||
string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
|
||
",height=" + std::to_string(media_.height);
|
||
GstCaps *caps = gst_caps_from_string(capstring.c_str());
|
||
if (!gst_video_info_from_caps (&v_frame_video_info_, caps)) {
|
||
Log::Warning("MediaPlayer %s Could not configure video frame info", std::to_string(id_).c_str());
|
||
failed_ = true;
|
||
return;
|
||
}
|
||
|
||
// setup uridecodebin
|
||
if (force_software_decoding_) {
|
||
g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "decoder")), "force-sw-decoders", true, NULL);
|
||
}
|
||
|
||
// setup appsink
|
||
GstElement *sink = gst_bin_get_by_name (GST_BIN (pipeline_), "sink");
|
||
if (!sink) {
|
||
Log::Warning("MediaPlayer %s Could not configure sink", std::to_string(id_).c_str());
|
||
failed_ = true;
|
||
return;
|
||
}
|
||
|
||
// instruct the sink to send samples synched in time
|
||
gst_base_sink_set_sync (GST_BASE_SINK(sink), true);
|
||
|
||
// instruct sink to use the required caps
|
||
gst_app_sink_set_caps (GST_APP_SINK(sink), caps);
|
||
|
||
// Instruct appsink to drop old buffers when the maximum amount of queued buffers is reached.
|
||
gst_app_sink_set_max_buffers( GST_APP_SINK(sink), 5);
|
||
gst_app_sink_set_drop (GST_APP_SINK(sink), true);
|
||
|
||
#ifdef USE_GST_APPSINK_CALLBACKS
|
||
// set the callbacks
|
||
GstAppSinkCallbacks callbacks;
|
||
callbacks.new_preroll = callback_new_preroll;
|
||
if (media_.isimage) {
|
||
callbacks.eos = NULL;
|
||
callbacks.new_sample = NULL;
|
||
}
|
||
else {
|
||
callbacks.eos = callback_end_of_stream;
|
||
callbacks.new_sample = callback_new_sample;
|
||
}
|
||
gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL);
|
||
gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false);
|
||
#else
|
||
// connect signals callbacks
|
||
g_signal_connect(G_OBJECT(sink), "new-preroll", G_CALLBACK (callback_new_preroll), this);
|
||
if (!media_.isimage) {
|
||
g_signal_connect(G_OBJECT(sink), "new-sample", G_CALLBACK (callback_new_sample), this);
|
||
g_signal_connect(G_OBJECT(sink), "eos", G_CALLBACK (callback_end_of_stream), this);
|
||
}
|
||
gst_app_sink_set_emit_signals (GST_APP_SINK(sink), true);
|
||
#endif
|
||
|
||
// done with ref to sink
|
||
gst_object_unref (sink);
|
||
gst_caps_unref (caps);
|
||
|
||
#ifdef USE_GST_OPENGL_SYNC_HANDLER
|
||
// capture bus signals to force a unique opengl context for all GST elements
|
||
Rendering::LinkPipeline(GST_PIPELINE (pipeline_));
|
||
#endif
|
||
|
||
// set to desired state (PLAY or PAUSE)
|
||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, desired_state_);
|
||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||
Log::Warning("MediaPlayer %s Could not open '%s'", std::to_string(id_).c_str(), uri_.c_str());
|
||
failed_ = true;
|
||
return;
|
||
}
|
||
|
||
// in case discoverer failed to get duration
|
||
if (timeline_.end() == GST_CLOCK_TIME_NONE) {
|
||
gint64 d = GST_CLOCK_TIME_NONE;
|
||
if ( gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &d) )
|
||
timeline_.setEnd(d);
|
||
}
|
||
|
||
// all good
|
||
Log::Info("MediaPlayer %s Opened '%s' (%s %d x %d)", std::to_string(id_).c_str(),
|
||
uri_.c_str(), media_.codec_name.c_str(), media_.width, media_.height);
|
||
|
||
Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
|
||
timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());
|
||
|
||
opened_ = true;
|
||
|
||
// register media player
|
||
MediaPlayer::registered_.push_back(this);
|
||
}
|
||
|
||
bool MediaPlayer::isOpen() const
|
||
{
|
||
return opened_;
|
||
}
|
||
|
||
bool MediaPlayer::failed() const
|
||
{
|
||
return failed_;
|
||
}
|
||
|
||
void MediaPlayer::Frame::unmap()
|
||
{
|
||
if ( full )
|
||
gst_video_frame_unmap(&vframe);
|
||
full = false;
|
||
}
|
||
|
||
void MediaPlayer::close()
|
||
{
|
||
// not openned?
|
||
if (!opened_) {
|
||
// wait for loading to finish
|
||
if (discoverer_.valid())
|
||
discoverer_.wait();
|
||
// nothing else to change
|
||
return;
|
||
}
|
||
|
||
// un-ready the media player
|
||
opened_ = false;
|
||
|
||
// clean up GST
|
||
if (pipeline_ != nullptr) {
|
||
|
||
// force flush
|
||
GstState state;
|
||
gst_element_send_event(pipeline_, gst_event_new_seek (1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
|
||
GST_SEEK_TYPE_NONE, 0, GST_SEEK_TYPE_NONE, 0) );
|
||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||
|
||
// end pipeline
|
||
gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||
|
||
gst_object_unref (pipeline_);
|
||
pipeline_ = nullptr;
|
||
}
|
||
|
||
// cleanup eventual remaining frame memory
|
||
for(guint i = 0; i < N_VFRAME; i++) {
|
||
frame_[i].access.lock();
|
||
frame_[i].unmap();
|
||
frame_[i].access.unlock();
|
||
}
|
||
write_index_ = 0;
|
||
last_index_ = 0;
|
||
|
||
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
Log::Info("MediaPlayer %s closed", std::to_string(id_).c_str());
|
||
#endif
|
||
|
||
// unregister media player
|
||
MediaPlayer::registered_.remove(this);
|
||
}
|
||
|
||
|
||
guint MediaPlayer::width() const
|
||
{
|
||
return media_.width;
|
||
}
|
||
|
||
guint MediaPlayer::height() const
|
||
{
|
||
return media_.height;
|
||
}
|
||
|
||
float MediaPlayer::aspectRatio() const
|
||
{
|
||
return static_cast<float>(media_.par_width) / static_cast<float>(media_.height);
|
||
}
|
||
|
||
GstClockTime MediaPlayer::position()
|
||
{
|
||
if (position_ == GST_CLOCK_TIME_NONE && pipeline_ != nullptr) {
|
||
gint64 p = GST_CLOCK_TIME_NONE;
|
||
if ( gst_element_query_position (pipeline_, GST_FORMAT_TIME, &p) )
|
||
position_ = p;
|
||
}
|
||
|
||
return position_;
|
||
}
|
||
|
||
void MediaPlayer::enable(bool on)
|
||
{
|
||
if ( !opened_ || pipeline_ == nullptr)
|
||
return;
|
||
|
||
if ( enabled_ != on ) {
|
||
|
||
enabled_ = on;
|
||
|
||
// default to pause
|
||
GstState requested_state = GST_STATE_PAUSED;
|
||
|
||
// unpause only if enabled
|
||
if (enabled_) {
|
||
requested_state = desired_state_;
|
||
}
|
||
|
||
// apply state change
|
||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, requested_state);
|
||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||
Log::Warning("MediaPlayer %s Failed to enable", std::to_string(id_).c_str());
|
||
failed_ = true;
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
bool MediaPlayer::isEnabled() const
|
||
{
|
||
return enabled_;
|
||
}
|
||
|
||
bool MediaPlayer::isImage() const
|
||
{
|
||
return media_.isimage;
|
||
}
|
||
|
||
std::string MediaPlayer::decoderName()
|
||
{
|
||
// decoder_name_ not initialized
|
||
if (decoder_name_.empty()) {
|
||
// try to know if it is a hardware decoder
|
||
decoder_name_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
|
||
// nope, then it is a sofware decoder
|
||
if (decoder_name_.empty())
|
||
decoder_name_ = "software";
|
||
}
|
||
|
||
return decoder_name_;
|
||
}
|
||
|
||
bool MediaPlayer::softwareDecodingForced()
|
||
{
|
||
return force_software_decoding_;
|
||
}
|
||
|
||
void MediaPlayer::setSoftwareDecodingForced(bool on)
|
||
{
|
||
bool need_reload = force_software_decoding_ != on;
|
||
|
||
// set parameter
|
||
force_software_decoding_ = on;
|
||
decoder_name_ = "";
|
||
|
||
// changing state requires reload
|
||
if (need_reload)
|
||
reopen();
|
||
}
|
||
|
||
void MediaPlayer::play(bool on)
|
||
{
|
||
// ignore if disabled, and cannot play an image
|
||
if (!enabled_ || media_.isimage)
|
||
return;
|
||
|
||
// request state
|
||
GstState requested_state = on ? GST_STATE_PLAYING : GST_STATE_PAUSED;
|
||
|
||
// ignore if requesting twice same state
|
||
if (desired_state_ == requested_state)
|
||
return;
|
||
|
||
// accept request to the desired state
|
||
desired_state_ = requested_state;
|
||
|
||
// if not ready yet, the requested state will be handled later
|
||
if ( pipeline_ == nullptr )
|
||
return;
|
||
|
||
// requesting to play, but stopped at end of stream : rewind first !
|
||
if ( desired_state_ == GST_STATE_PLAYING) {
|
||
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|
||
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
|
||
rewind();
|
||
}
|
||
|
||
// all ready, apply state change immediately
|
||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, desired_state_);
|
||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||
Log::Warning("MediaPlayer %s Failed to play", std::to_string(id_).c_str());
|
||
failed_ = true;
|
||
}
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
else if (on)
|
||
Log::Info("MediaPlayer %s Start", std::to_string(id_).c_str());
|
||
else
|
||
Log::Info("MediaPlayer %s Stop [%ld]", std::to_string(id_).c_str(), position());
|
||
#endif
|
||
|
||
}
|
||
|
||
bool MediaPlayer::isPlaying(bool testpipeline) const
|
||
{
|
||
// image cannot play
|
||
if (media_.isimage)
|
||
return false;
|
||
|
||
// if not ready yet, answer with requested state
|
||
if ( !testpipeline || pipeline_ == nullptr || !enabled_)
|
||
return desired_state_ == GST_STATE_PLAYING;
|
||
|
||
// if ready, answer with actual state
|
||
GstState state;
|
||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||
return state == GST_STATE_PLAYING;
|
||
}
|
||
|
||
|
||
MediaPlayer::LoopMode MediaPlayer::loop() const
|
||
{
|
||
return loop_;
|
||
}
|
||
|
||
void MediaPlayer::setLoop(MediaPlayer::LoopMode mode)
|
||
{
|
||
loop_ = mode;
|
||
}
|
||
|
||
void MediaPlayer::rewind()
|
||
{
|
||
if (!enabled_ || !media_.seekable)
|
||
return;
|
||
|
||
// playing forward, loop to begin
|
||
if (rate_ > 0.0) {
|
||
// begin is the end of a gab which includes the first PTS (if exists)
|
||
// normal case, begin is zero
|
||
execute_seek_command( timeline_.next(0) );
|
||
}
|
||
// playing backward, loop to endTimeInterval gap;
|
||
else {
|
||
// end is the start of a gab which includes the last PTS (if exists)
|
||
// normal case, end is last frame
|
||
execute_seek_command( timeline_.previous(timeline_.last()) );
|
||
}
|
||
}
|
||
|
||
|
||
void MediaPlayer::step()
|
||
{
|
||
// useful only when Paused
|
||
if (!enabled_ || isPlaying())
|
||
return;
|
||
|
||
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|
||
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
|
||
rewind();
|
||
|
||
// step
|
||
gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE));
|
||
}
|
||
|
||
bool MediaPlayer::go_to(GstClockTime pos)
|
||
{
|
||
bool ret = false;
|
||
TimeInterval gap;
|
||
if (pos != GST_CLOCK_TIME_NONE ) {
|
||
|
||
GstClockTime jumpPts = pos;
|
||
|
||
if (timeline_.getGapAt(pos, gap)) {
|
||
// if in a gap, find closest seek target
|
||
if (gap.is_valid()) {
|
||
// jump in one or the other direction
|
||
jumpPts = (rate_>0.f) ? gap.end : gap.begin;
|
||
}
|
||
}
|
||
|
||
if (ABS_DIFF (position_, jumpPts) > 2 * timeline_.step() ) {
|
||
ret = true;
|
||
seek( jumpPts );
|
||
}
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
void MediaPlayer::seek(GstClockTime pos)
|
||
{
|
||
if (!enabled_ || !media_.seekable || seeking_)
|
||
return;
|
||
|
||
// apply seek
|
||
GstClockTime target = CLAMP(pos, timeline_.begin(), timeline_.end());
|
||
execute_seek_command(target);
|
||
|
||
}
|
||
|
||
void MediaPlayer::jump()
|
||
{
|
||
if (!enabled_ || !isPlaying())
|
||
return;
|
||
|
||
gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_BUFFERS, 1, 30.f * ABS(rate_), TRUE, FALSE));
|
||
}
|
||
|
||
void MediaPlayer::init_texture(guint index)
|
||
{
|
||
glActiveTexture(GL_TEXTURE0);
|
||
glGenTextures(1, &textureindex_);
|
||
glBindTexture(GL_TEXTURE_2D, textureindex_);
|
||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, media_.width, media_.height);
|
||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, frame_[index].vframe.data[0]);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||
|
||
if (!media_.isimage) {
|
||
|
||
// set pbo image size
|
||
pbo_size_ = media_.height * media_.width * 4;
|
||
|
||
// create pixel buffer objects,
|
||
if (pbo_[0])
|
||
glDeleteBuffers(2, pbo_);
|
||
glGenBuffers(2, pbo_);
|
||
|
||
for(int i = 0; i < 2; i++ ) {
|
||
// create 2 PBOs
|
||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[i]);
|
||
// glBufferDataARB with NULL pointer reserves only memory space.
|
||
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
|
||
// fill in with reset picture
|
||
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
|
||
if (ptr) {
|
||
// update data directly on the mapped buffer
|
||
memmove(ptr, frame_[index].vframe.data[0], pbo_size_);
|
||
// release pointer to mapping buffer
|
||
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
|
||
}
|
||
else {
|
||
// did not work, disable PBO
|
||
glDeleteBuffers(2, pbo_);
|
||
pbo_[0] = pbo_[1] = 0;
|
||
pbo_size_ = 0;
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
// should be good to go, wrap it up
|
||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||
pbo_index_ = 0;
|
||
pbo_next_index_ = 1;
|
||
|
||
// // now that a frame is ready, and once only, browse into the pipeline
|
||
// // for possible hadrware decoding plugins used. Empty string means none.
|
||
// hardware_decoder_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
|
||
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
Log::Info("MediaPlayer %s uses OpenGL PBO texturing.", std::to_string(id_).c_str());
|
||
#endif
|
||
|
||
}
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
}
|
||
|
||
|
||
void MediaPlayer::fill_texture(guint index)
|
||
{
|
||
// is this the first frame ?
|
||
if (textureindex_ < 1)
|
||
{
|
||
// initialize texture
|
||
init_texture(index);
|
||
}
|
||
else {
|
||
glBindTexture(GL_TEXTURE_2D, textureindex_);
|
||
|
||
// use dual Pixel Buffer Object
|
||
if (pbo_size_ > 0) {
|
||
// In dual PBO mode, increment current index first then get the next index
|
||
pbo_index_ = (pbo_index_ + 1) % 2;
|
||
pbo_next_index_ = (pbo_index_ + 1) % 2;
|
||
|
||
// bind PBO to read pixels
|
||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_index_]);
|
||
// copy pixels from PBO to texture object
|
||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||
// bind the next PBO to write pixels
|
||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);
|
||
// See http://www.songho.ca/opengl/gl_pbo.html#map for more details
|
||
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
|
||
// map the buffer object into client's memory
|
||
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
|
||
if (ptr) {
|
||
// update data directly on the mapped buffer
|
||
// NB : equivalent but faster (memmove instead of memcpy ?) than
|
||
// glNamedBufferSubData(pboIds[nextIndex], 0, imgsize, vp->getBuffer())
|
||
memmove(ptr, frame_[index].vframe.data[0], pbo_size_);
|
||
|
||
// release pointer to mapping buffer
|
||
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
|
||
}
|
||
// done with PBO
|
||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||
}
|
||
else {
|
||
// without PBO, use standard opengl (slower)
|
||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, frame_[index].vframe.data[0]);
|
||
}
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
}
|
||
}
|
||
|
||
void MediaPlayer::update()
|
||
{
|
||
// discard
|
||
if (failed_)
|
||
return;
|
||
|
||
// not ready yet
|
||
if (!opened_) {
|
||
if (discoverer_.valid()) {
|
||
// try to get info from discoverer
|
||
if (discoverer_.wait_for( std::chrono::milliseconds(4) ) == std::future_status::ready )
|
||
{
|
||
media_ = discoverer_.get();
|
||
// if its ok, open the media
|
||
if (media_.valid) {
|
||
timeline_.setEnd( media_.end );
|
||
timeline_.setStep( media_.dt );
|
||
execute_open();
|
||
}
|
||
else {
|
||
Log::Warning("MediaPlayer %s Loading cancelled", std::to_string(id_).c_str());
|
||
failed_ = true;
|
||
}
|
||
}
|
||
}
|
||
// wait next frame to display
|
||
return;
|
||
}
|
||
|
||
// prevent unnecessary updates: disabled or already filled image
|
||
if (!enabled_ || (media_.isimage && textureindex_>0 ) )
|
||
return;
|
||
|
||
// local variables before trying to update
|
||
guint read_index = 0;
|
||
bool need_loop = false;
|
||
|
||
// locked access to current index
|
||
index_lock_.lock();
|
||
// get the last frame filled from fill_frame()
|
||
read_index = last_index_;
|
||
// // Do NOT miss and jump directly (after seek) to a pre-roll
|
||
// for (guint i = 0; i < N_VFRAME; ++i) {
|
||
// if (frame_[i].status == PREROLL) {
|
||
// read_index = i;
|
||
// break;
|
||
// }
|
||
// }
|
||
// unlock access to index change
|
||
index_lock_.unlock();
|
||
|
||
// lock frame while reading it
|
||
frame_[read_index].access.lock();
|
||
|
||
// do not fill a frame twice
|
||
if (frame_[read_index].status != INVALID ) {
|
||
|
||
// is this an End-of-Stream frame ?
|
||
if (frame_[read_index].status == EOS )
|
||
{
|
||
// will execute seek command below (after unlock)
|
||
need_loop = true;
|
||
}
|
||
// otherwise just fill non-empty SAMPLE or PREROLL
|
||
else if (frame_[read_index].full)
|
||
{
|
||
// fill the texture with the frame at reading index
|
||
fill_texture(read_index);
|
||
|
||
// double update for pre-roll frame and dual PBO (ensure frame is displayed now)
|
||
if ( (frame_[read_index].status == PREROLL || seeking_ ) && pbo_size_ > 0)
|
||
fill_texture(read_index);
|
||
|
||
// free frame
|
||
frame_[read_index].unmap();
|
||
}
|
||
|
||
// we just displayed a vframe : set position time to frame PTS
|
||
position_ = frame_[read_index].position;
|
||
|
||
// avoid reading it again
|
||
frame_[read_index].status = INVALID;
|
||
}
|
||
|
||
// unkock frame after reading it
|
||
frame_[read_index].access.unlock();
|
||
|
||
// if already seeking (asynch)
|
||
if (seeking_) {
|
||
// request status update to pipeline (re-sync gst thread)
|
||
GstState state;
|
||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||
// seek should be resolved next frame
|
||
seeking_ = false;
|
||
// do NOT do another seek yet
|
||
}
|
||
// otherwise check for need to seek (pipeline management)
|
||
else {
|
||
// manage timeline: test if position falls into a gap
|
||
TimeInterval gap;
|
||
if (position_ != GST_CLOCK_TIME_NONE && timeline_.getGapAt(position_, gap)) {
|
||
// if in a gap, seek to next section
|
||
if (gap.is_valid()) {
|
||
// jump in one or the other direction
|
||
GstClockTime jumpPts = (rate_>0.f) ? gap.end : gap.begin;
|
||
// seek to next valid time (if not beginnig or end of timeline)
|
||
if (jumpPts > timeline_.first() && jumpPts < timeline_.last())
|
||
seek( jumpPts );
|
||
// otherwise, we should loop
|
||
else
|
||
need_loop = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// manage loop mode
|
||
if (need_loop) {
|
||
execute_loop_command();
|
||
}
|
||
|
||
}
|
||
|
||
void MediaPlayer::execute_loop_command()
|
||
{
|
||
if (loop_==LOOP_REWIND) {
|
||
rewind();
|
||
}
|
||
else if (loop_==LOOP_BIDIRECTIONAL) {
|
||
rate_ *= - 1.f;
|
||
execute_seek_command();
|
||
}
|
||
else { //LOOP_NONE
|
||
play(false);
|
||
}
|
||
}
|
||
|
||
void MediaPlayer::execute_seek_command(GstClockTime target)
|
||
{
|
||
if ( pipeline_ == nullptr || !media_.seekable )
|
||
return;
|
||
|
||
// seek position : default to target
|
||
GstClockTime seek_pos = target;
|
||
|
||
// no target given
|
||
if (target == GST_CLOCK_TIME_NONE)
|
||
// create seek event with current position (rate changed ?)
|
||
seek_pos = position_;
|
||
// target is given but useless
|
||
else if ( ABS_DIFF(target, position_) < timeline_.step()) {
|
||
// ignore request
|
||
return;
|
||
}
|
||
|
||
// seek with flush (always)
|
||
int seek_flags = GST_SEEK_FLAG_FLUSH;
|
||
// seek with trick mode if fast speed
|
||
if ( ABS(rate_) > 1.0 )
|
||
seek_flags |= GST_SEEK_FLAG_TRICKMODE;
|
||
|
||
// create seek event depending on direction
|
||
GstEvent *seek_event = nullptr;
|
||
if (rate_ > 0) {
|
||
seek_event = gst_event_new_seek (rate_, GST_FORMAT_TIME, (GstSeekFlags) seek_flags,
|
||
GST_SEEK_TYPE_SET, seek_pos, GST_SEEK_TYPE_END, 0);
|
||
}
|
||
else {
|
||
seek_event = gst_event_new_seek (rate_, GST_FORMAT_TIME, (GstSeekFlags) seek_flags,
|
||
GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, seek_pos);
|
||
}
|
||
|
||
// Send the event (ASYNC)
|
||
if (seek_event && !gst_element_send_event(pipeline_, seek_event) )
|
||
Log::Warning("MediaPlayer %s Seek failed", std::to_string(id_).c_str());
|
||
else {
|
||
seeking_ = true;
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
Log::Info("MediaPlayer %s Seek %ld %.1f", std::to_string(id_).c_str(), seek_pos, rate_);
|
||
#endif
|
||
}
|
||
|
||
}
|
||
|
||
void MediaPlayer::setPlaySpeed(double s)
|
||
{
|
||
if (media_.isimage)
|
||
return;
|
||
|
||
// bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
|
||
rate_ = CLAMP(s, -MAX_PLAY_SPEED, MAX_PLAY_SPEED);
|
||
// skip interval [-MIN_PLAY_SPEED MIN_PLAY_SPEED]
|
||
if (ABS(rate_) < MIN_PLAY_SPEED)
|
||
rate_ = SIGN(rate_) * MIN_PLAY_SPEED;
|
||
|
||
// apply with seek
|
||
execute_seek_command();
|
||
}
|
||
|
||
double MediaPlayer::playSpeed() const
|
||
{
|
||
return rate_;
|
||
}
|
||
|
||
Timeline *MediaPlayer::timeline()
|
||
{
|
||
return &timeline_;
|
||
}
|
||
|
||
float MediaPlayer::currentTimelineFading()
|
||
{
|
||
return timeline_.fadingAt(position_);
|
||
}
|
||
|
||
void MediaPlayer::setTimeline(const Timeline &tl)
|
||
{
|
||
timeline_ = tl;
|
||
}
|
||
|
||
//void MediaPlayer::toggleGapInTimeline(GstClockTime from, GstClockTime to)
|
||
//{
|
||
// return timeline.toggleGaps(from, to);
|
||
//}
|
||
|
||
MediaInfo MediaPlayer::media() const
|
||
{
|
||
return media_;
|
||
}
|
||
|
||
std::string MediaPlayer::uri() const
|
||
{
|
||
return uri_;
|
||
}
|
||
|
||
std::string MediaPlayer::filename() const
|
||
{
|
||
return filename_;
|
||
}
|
||
|
||
double MediaPlayer::frameRate() const
|
||
{
|
||
return static_cast<double>(media_.framerate_n) / static_cast<double>(media_.framerate_d);;
|
||
}
|
||
|
||
double MediaPlayer::updateFrameRate() const
|
||
{
|
||
return timecount_.frameRate();
|
||
}
|
||
|
||
|
||
// CALLBACKS
|
||
|
||
bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
|
||
{
|
||
// Do NOT overwrite an unread EOS
|
||
if ( frame_[write_index_].status == EOS )
|
||
write_index_ = (write_index_ + 1) % N_VFRAME;
|
||
|
||
// lock access to frame
|
||
frame_[write_index_].access.lock();
|
||
|
||
// always empty frame before filling it again
|
||
frame_[write_index_].unmap();
|
||
|
||
// accept status of frame received
|
||
frame_[write_index_].status = status;
|
||
|
||
// a buffer is given (not EOS)
|
||
if (buf != NULL) {
|
||
|
||
// get the frame from buffer
|
||
if ( !gst_video_frame_map (&frame_[write_index_].vframe, &v_frame_video_info_, buf, GST_MAP_READ ) )
|
||
{
|
||
Log::Info("MediaPlayer %s Failed to map the video buffer", std::to_string(id_).c_str());
|
||
// free access to frame & exit
|
||
frame_[write_index_].status = INVALID;
|
||
frame_[write_index_].access.unlock();
|
||
return false;
|
||
}
|
||
|
||
// successfully filled the frame
|
||
frame_[write_index_].full = true;
|
||
|
||
// validate frame format
|
||
if( GST_VIDEO_INFO_IS_RGB(&(frame_[write_index_].vframe).info) && GST_VIDEO_INFO_N_PLANES(&(frame_[write_index_].vframe).info) == 1)
|
||
{
|
||
// set presentation time stamp
|
||
frame_[write_index_].position = buf->pts;
|
||
|
||
// set the start position (i.e. pts of first frame we got)
|
||
if (timeline_.first() == GST_CLOCK_TIME_NONE) {
|
||
timeline_.setFirst(buf->pts);
|
||
}
|
||
}
|
||
// full but invalid frame : will be deleted next iteration
|
||
// (should never happen)
|
||
else {
|
||
#ifdef MEDIA_PLAYER_DEBUG
|
||
Log::Info("MediaPlayer %s Received an Invalid frame", std::to_string(id_).c_str());
|
||
#endif
|
||
frame_[write_index_].status = INVALID;
|
||
frame_[write_index_].access.unlock();
|
||
return false;
|
||
}
|
||
}
|
||
// else; null buffer for EOS: give a position
|
||
else {
|
||
frame_[write_index_].status = EOS;
|
||
frame_[write_index_].position = rate_ > 0.0 ? timeline_.end() : timeline_.begin();
|
||
}
|
||
|
||
// unlock access to frame
|
||
frame_[write_index_].access.unlock();
|
||
|
||
// lock access to change current index (very quick)
|
||
index_lock_.lock();
|
||
// indicate update() that this is the last frame filled (and unlocked)
|
||
last_index_ = write_index_;
|
||
// unlock access to index change
|
||
index_lock_.unlock();
|
||
|
||
// for writing, we will access the next in stack
|
||
write_index_ = (write_index_ + 1) % N_VFRAME;
|
||
|
||
// calculate actual FPS of update
|
||
timecount_.tic();
|
||
|
||
return true;
|
||
}
|
||
|
||
void MediaPlayer::callback_end_of_stream (GstAppSink *, gpointer p)
|
||
{
|
||
MediaPlayer *m = static_cast<MediaPlayer *>(p);
|
||
if (m && m->opened_) {
|
||
m->fill_frame(NULL, MediaPlayer::EOS);
|
||
}
|
||
}
|
||
|
||
GstFlowReturn MediaPlayer::callback_new_preroll (GstAppSink *sink, gpointer p)
|
||
{
|
||
GstFlowReturn ret = GST_FLOW_OK;
|
||
|
||
// blocking read pre-roll samples
|
||
GstSample *sample = gst_app_sink_pull_preroll(sink);
|
||
|
||
// if got a valid sample
|
||
if (sample != NULL) {
|
||
|
||
// send frames to media player only if ready
|
||
MediaPlayer *m = static_cast<MediaPlayer *>(p);
|
||
if (m && m->opened_) {
|
||
|
||
// get buffer from sample
|
||
GstBuffer *buf = gst_sample_get_buffer (sample);
|
||
|
||
// fill frame from buffer
|
||
if ( !m->fill_frame(buf, MediaPlayer::PREROLL) )
|
||
ret = GST_FLOW_ERROR;
|
||
// loop negative rate: emulate an EOS
|
||
else if (m->playSpeed() < 0.f && !(buf->pts > 0) ) {
|
||
m->fill_frame(NULL, MediaPlayer::EOS);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
ret = GST_FLOW_FLUSHING;
|
||
|
||
// release sample
|
||
gst_sample_unref (sample);
|
||
|
||
return ret;
|
||
}
|
||
|
||
GstFlowReturn MediaPlayer::callback_new_sample (GstAppSink *sink, gpointer p)
|
||
{
|
||
GstFlowReturn ret = GST_FLOW_OK;
|
||
|
||
// non-blocking read new sample
|
||
GstSample *sample = gst_app_sink_pull_sample(sink);
|
||
|
||
// if got a valid sample
|
||
if (sample != NULL && !gst_app_sink_is_eos (sink)) {
|
||
|
||
// send frames to media player only if ready
|
||
MediaPlayer *m = static_cast<MediaPlayer *>(p);
|
||
if (m && m->opened_) {
|
||
|
||
// get buffer from sample (valid until sample is released)
|
||
GstBuffer *buf = gst_sample_get_buffer (sample) ;
|
||
|
||
// fill frame with buffer
|
||
if ( !m->fill_frame(buf, MediaPlayer::SAMPLE) )
|
||
ret = GST_FLOW_ERROR;
|
||
// loop negative rate: emulate an EOS
|
||
else if (m->playSpeed() < 0.f && !(buf->pts > 0) ) {
|
||
m->fill_frame(NULL, MediaPlayer::EOS);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
ret = GST_FLOW_FLUSHING;
|
||
|
||
// release sample
|
||
gst_sample_unref (sample);
|
||
|
||
return ret;
|
||
}
|
||
|
||
|
||
|
||
MediaPlayer::TimeCounter::TimeCounter()
|
||
{
|
||
timer = g_timer_new ();
|
||
}
|
||
|
||
MediaPlayer::TimeCounter::~TimeCounter()
|
||
{
|
||
g_free(timer);
|
||
}
|
||
|
||
void MediaPlayer::TimeCounter::tic ()
|
||
{
|
||
double dt = g_timer_elapsed (timer, NULL) * 1000.0;
|
||
g_timer_start(timer);
|
||
|
||
// calculate instantaneous framerate
|
||
// Exponential moving averate with previous framerate to filter jitter
|
||
if (dt > 1.0)
|
||
fps = CLAMP( 0.5 * fps + 500.0 / dt, 0.0, 1000.0) ;
|
||
}
|
||
|