Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
420 lines
13 KiB
C++
420 lines
13 KiB
C++
#include <functional>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <ostream>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#include "assert.h"
|
|
|
|
#ifndef USE_GI_MODULE
|
|
// no inline as the implementation is provided by other TUs
|
|
// #define GI_INLINE 1
|
|
#include <gst/gst.hpp>
|
|
#else
|
|
// optional, but recommended if gi macros are used
|
|
#include <gi/gi_inc.hpp>
|
|
// we also use some gst macros and api
|
|
#include <gst/gst.h>
|
|
// import used modules
|
|
import gi.repo.gobject;
|
|
import gi.repo.gst;
|
|
#endif
|
|
|
|
namespace GLib = gi::repository::GLib;
|
|
namespace Gst = gi::repository::Gst;
|
|
namespace GObject_ = gi::repository::GObject;
|
|
|
|
void
|
|
say(const std::string &msg)
|
|
{
|
|
std::cout << msg << std::endl;
|
|
}
|
|
|
|
void
|
|
die(const std::string &why)
|
|
{
|
|
std::cerr << why << std::endl;
|
|
exit(2);
|
|
}
|
|
|
|
class Player
|
|
{
|
|
typedef Player self;
|
|
|
|
GLib::MainLoop loop_;
|
|
std::string url_;
|
|
|
|
Gst::Element playbin_;
|
|
bool live_ = false;
|
|
Gst::Element vfilter_;
|
|
Gst::Element afilter_;
|
|
std::map<std::string, Gst::Element> filter_;
|
|
GLib::SourceScopedConnection monitor_;
|
|
GObject_::SignalConnection busconn_;
|
|
|
|
public:
|
|
Player(GLib::MainLoop loop, const std::string &url) : loop_(loop), url_(url)
|
|
{
|
|
playbin_ = Gst::ElementFactory::make("playbin");
|
|
assert(playbin_);
|
|
// get a property
|
|
// this one is known at introspection time
|
|
// (so type is known and suitable checked)
|
|
auto name = playbin_.property_name().get();
|
|
std::cout << "created playbin " << name << std::endl;
|
|
}
|
|
|
|
void start()
|
|
{
|
|
// set some property
|
|
playbin_.set_property("uri", url_);
|
|
// ... or several ones
|
|
playbin_.set_properties("volume", 0.5, "mute", false);
|
|
// ... non-primitive also possible
|
|
vfilter_ = Gst::ElementFactory::make("identity", "vfilter");
|
|
afilter_ = Gst::ElementFactory::make("identity", "afilter");
|
|
playbin_.set_properties("video-filter", vfilter_, "audio-filter", afilter_);
|
|
|
|
// connect some; find out what the source element is
|
|
// signal not known to introspection, so have to provide signature here
|
|
playbin_.connect<void(Gst::Element, Gst::Element)>(
|
|
"source-setup", gi::mem_fun(&self::on_source_setup, this));
|
|
// for a known signal, it is a bit easier to connect
|
|
auto bus = playbin_.get_bus();
|
|
bus.add_signal_watch();
|
|
// signal connect; using introspected (hence checked) signal definition
|
|
// could do without the slot step here and directly pass the lambda,
|
|
// but wrapping it this way allows combining this with the returned
|
|
// (plain) id into a scoped connection guard
|
|
auto slot = bus.signal_message().slot(gi::mem_fun(&self::on_message, this));
|
|
busconn_ =
|
|
gi::make_connection(bus.signal_message().connect(slot), slot, bus);
|
|
// again, if the guard is not desired; the following simply suffices
|
|
if (false)
|
|
bus.signal_message().connect(gi::mem_fun(&self::on_message, this));
|
|
// if the signal signature is somehow not supported
|
|
// then the following is a fallback manual method
|
|
// which still allows to use lambda (and such)
|
|
// and also provides some ownership management
|
|
if (false) {
|
|
auto h = [](GstBus *, GstMessage *) {
|
|
// dummy no-op
|
|
};
|
|
bus.connect_unchecked<void(GstBus *, GstMessage *)>("message", h);
|
|
}
|
|
// likewise, if a callback is to be used whose arguments are not supported
|
|
// or in a function that is not supported, then the following is a fallback
|
|
// which still allows to use lambda (and such)
|
|
// and also provides some ownership management
|
|
if (false) {
|
|
auto h = []() { return false; };
|
|
auto cb = new gi::callback_wrapper<gboolean(), false>(h);
|
|
// now the above pointer is managed as user data
|
|
// and will be suitable destroyed when needed
|
|
g_idle_add_full(0, &cb->wrapper, cb, &cb->destroy);
|
|
// in case of a typical single-use async callback (with no GDestroyNotify)
|
|
// use true as AUTODESTROY template parameter
|
|
// (then it will auto-clean up after invoking callback)
|
|
}
|
|
// alternatively, maybe there is some API that accepts a Closure
|
|
// the following are some convenient ways to get a closure
|
|
if (false) {
|
|
GLib::LogFunc h = [](gi::cstring_v log_domain,
|
|
GLib::LogLevelFlags log_level,
|
|
gi::cstring_v message) {
|
|
(void)log_domain;
|
|
(void)log_level;
|
|
(void)message;
|
|
};
|
|
GObject_::Closure::from_callback(h);
|
|
}
|
|
// likewise, but with a bit more manual (C++) signature specification
|
|
if (false) {
|
|
auto h = [](GObject_::Object, int) {};
|
|
GObject_::Closure::from_functor<void(GObject_::Object, int)>(h);
|
|
}
|
|
|
|
say("Setting pipeline to PAUSED ...");
|
|
auto ret = playbin_.set_state(Gst::State::PAUSED_);
|
|
if (ret == Gst::StateChangeReturn::FAILURE_) {
|
|
die("Pipeline does not want to pause");
|
|
} else if (ret == Gst::StateChangeReturn::NO_PREROLL_) {
|
|
say("Pipeline is live and does not need PREROLL ...");
|
|
live_ = true;
|
|
} else if (ret == Gst::StateChangeReturn::ASYNC_) {
|
|
say("Pipeline is PREROLLING ...");
|
|
}
|
|
|
|
// inspect after a while
|
|
GLib::timeout_add_seconds(2, [this]() {
|
|
inspect();
|
|
return GLib::SOURCE_REMOVE_;
|
|
});
|
|
|
|
// some regular progress reporting ...
|
|
GLib::SourceFunc func = [this]() {
|
|
progress();
|
|
return GLib::SOURCE_CONTINUE_;
|
|
};
|
|
// the introspected function returns a plain id,
|
|
// which can be used in the usual way to disconnect
|
|
// e.g. at destructor time of owning object
|
|
// alternatively, a helper scoped object can take care of that
|
|
monitor_ = gi::make_connection(GLib::timeout_add_seconds(1, func), func);
|
|
// other such make_connection variations exist;
|
|
// e.g. for a signal connection, a probe callback
|
|
// (and can easily be custom added)
|
|
|
|
// add a pad probe; use a casual lambda
|
|
// but not too casual, mind (dangling) references/pointers though
|
|
filter_["video"] = vfilter_;
|
|
filter_["audio"] = afilter_;
|
|
for (auto &&p : filter_) {
|
|
if (p.second) {
|
|
Gst::Pad pad = p.second.get_static_pad("sink");
|
|
auto name = p.first;
|
|
auto handler = [name](Gst::Pad p, Gst::PadProbeInfo_Ref info) {
|
|
auto s = p.get_path_string();
|
|
s += "received " + name + " buffer";
|
|
auto buffer = info.get_buffer();
|
|
if (buffer) {
|
|
s += " of size ";
|
|
s += std::to_string(buffer.get_size());
|
|
}
|
|
return Gst::PadProbeReturn::REMOVE_;
|
|
};
|
|
pad.add_probe(Gst::PadProbeType::BUFFER_, handler);
|
|
}
|
|
}
|
|
|
|
// shamelessly demo some helpers that aid in caps/value handling
|
|
auto caps = Gst::Caps::new_empty_simple("video/x-raw");
|
|
caps.set_value("width", GObject_::Value(Gst::IntRange(240, 320)));
|
|
caps.set_value("pixel-aspect-ratio", GObject_::Value(Gst::Fraction(4, 3)));
|
|
caps.set_value(
|
|
"framerate", GObject_::Value(Gst::FractionRange({25, 1}, 30)));
|
|
// retrieving pretty much the same way
|
|
auto s = caps.get_structure(0);
|
|
auto par = s.get_value("pixel-aspect-ratio").get_value<Gst::Fraction>();
|
|
// helpers also stream to string properly
|
|
std::ostringstream oss;
|
|
oss << par;
|
|
oss << (Gst::FlagSet(1, 1) == Gst::FlagSet(2, 1));
|
|
// silly test code to exercise some operators
|
|
// Rank override allows for succinct numeric conversion
|
|
if (+Gst::Rank::PRIMARY_ + 0) {
|
|
// flags support various typical operations
|
|
(void)(Gst::PadProbeType::BUFFER_ | Gst::PadProbeType::BUFFER_LIST_);
|
|
}
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
if (playbin_)
|
|
playbin_.set_state(Gst::State::NULL_);
|
|
loop_.quit();
|
|
}
|
|
|
|
void on_message(Gst::Bus /*bus*/, Gst::Message_Ref msg)
|
|
{
|
|
auto &&src = msg.src_();
|
|
switch (msg.type_()) {
|
|
case Gst::MessageType::EOS_:
|
|
say("Got EOS from " + src.get_path_string());
|
|
stop();
|
|
break;
|
|
case Gst::MessageType::ERROR_: {
|
|
GLib::Error err;
|
|
gi::cstring debug;
|
|
msg.parse_error(&err, &debug);
|
|
say("Got error from " + src.get_path_string());
|
|
if (debug.size())
|
|
say("debug info:\n" + debug);
|
|
break;
|
|
}
|
|
case Gst::MessageType::STATE_CHANGED_:
|
|
/* only handle top-level case */
|
|
if (src != playbin_)
|
|
break;
|
|
Gst::State old, new_, pending;
|
|
msg.parse_state_changed(&old, &new_, &pending);
|
|
if (new_ == Gst::State::PAUSED_ && old == Gst::State::READY_)
|
|
playbin_.set_state(Gst::State::PLAYING_);
|
|
break;
|
|
default:
|
|
// never mind
|
|
break;
|
|
}
|
|
}
|
|
|
|
void on_source_setup(Gst::Element /*pb*/, Gst::Element src)
|
|
{
|
|
say("source is " + src.get_path_string());
|
|
}
|
|
|
|
void inspect()
|
|
{
|
|
std::ostringstream oss;
|
|
// this should work
|
|
auto bin = gi::object_cast<Gst::Bin>(playbin_);
|
|
assert(bin);
|
|
// a dynamically loaded element may implement a number of interfaces
|
|
// which is not known at compile-time
|
|
// the cast above can also cast to interface, which can be obtained as
|
|
// follows if known at introspection/compile time
|
|
auto cp = bin.interface_(gi::interface_tag<Gst::ChildProxy>());
|
|
oss << "player bin has " << cp.get_children_count() << " children"
|
|
<< std::endl;
|
|
// get some properties (dynamically, i.e. not known at introspection
|
|
// time) type will have to match (i.e. transformable) at runtime
|
|
auto n_v = playbin_.get_property<int>("n-video");
|
|
auto n_a = playbin_.get_property<int>("n-audio");
|
|
// minimal stuff
|
|
oss << "sample streams: video=" << n_v << ", audio=" << n_a << std::endl;
|
|
// show some tag info
|
|
for (auto &&p :
|
|
std::map<std::string, int>{{"video", n_v}, {"audio", n_a}}) {
|
|
for (int i = 0; i < p.second; ++i) {
|
|
// the argument's type should match the signal definition
|
|
// (cast if needed to make it so)
|
|
auto action = std::string("get-");
|
|
action += p.first + "-tags";
|
|
auto taglist = playbin_.emit<Gst::TagList>(action, i);
|
|
if (!taglist)
|
|
continue;
|
|
auto ntags = taglist.n_tags();
|
|
oss << p.first << " stream " << i << std::endl;
|
|
for (int j = 0; j < ntags; ++j) {
|
|
auto tname = taglist.nth_tag_name(j);
|
|
auto value = taglist.get_value_index(tname, 0);
|
|
#if GI_CONFIG_EXCEPTIONS
|
|
try {
|
|
#endif
|
|
auto sval = value.transform_value<std::string>();
|
|
oss << " " << tname << ": " << sval << std::endl;
|
|
#if GI_CONFIG_EXCEPTIONS
|
|
} catch (...) {
|
|
// could be object or otherwise, never mind
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// should also have caps here by now
|
|
// there are other ways to obtain this, but let's go this way here
|
|
for (auto &&p : filter_) {
|
|
if (p.second) {
|
|
Gst::Pad pad = p.second.get_static_pad("sink");
|
|
auto caps = pad.get_current_caps();
|
|
oss << p.first << " caps: " << caps.to_string() << std::endl;
|
|
}
|
|
}
|
|
|
|
say(oss.str());
|
|
|
|
say("Playbin elements:");
|
|
// Gst::Iterator could be used with native interface
|
|
// but a helper wrapper has been provided that supports ease-of-use as
|
|
// in ...
|
|
for (auto &&el : Gst::IteratorAdapter<Gst::Element>(bin.iterate_recurse()))
|
|
say(el.get_path_string());
|
|
say("");
|
|
}
|
|
|
|
static std::string time_to_str(Gst::ClockTime time)
|
|
{
|
|
// could be done otherwise
|
|
// but we have native C access at hand, so let's use that
|
|
auto s = g_strdup_printf("%" GST_TIME_FORMAT, GST_TIME_ARGS(time));
|
|
std::string ret(s);
|
|
g_free(s);
|
|
return ret;
|
|
}
|
|
|
|
void progress()
|
|
{
|
|
bool ok = true;
|
|
gint64 duration = -1, position = -1;
|
|
ok &= playbin_.query_duration(Gst::Format::TIME_, &duration);
|
|
ok &= playbin_.query_position(Gst::Format::TIME_, &position);
|
|
std::ostringstream oss;
|
|
if (ok) {
|
|
oss << "Duration: " << time_to_str(duration)
|
|
<< ", Position: " << time_to_str(position);
|
|
} else {
|
|
oss << "No progress info available";
|
|
}
|
|
say(oss.str());
|
|
}
|
|
};
|
|
|
|
#ifdef GI_CLASS_IMPL
|
|
// not used in the above, but serves as a subclass example
|
|
class ChattyBin : public Gst::impl::BinImpl
|
|
{
|
|
public:
|
|
#if 0
|
|
// this part is only needed if there is some conflict
|
|
// (among members of class and/or interfaces)
|
|
// otherwise it should be auto-detected
|
|
struct DefinitionData
|
|
{
|
|
GI_DEFINES_MEMBER(BinClassDef, add_element, true)
|
|
};
|
|
#endif
|
|
|
|
ChattyBin() : Gst::impl::BinImpl(this) {}
|
|
|
|
bool add_element_(Gst::Element element) noexcept override
|
|
{
|
|
say("adding element " + element.name_() + '\n');
|
|
return Gst::impl::BinImpl::add_element_(element);
|
|
}
|
|
};
|
|
#endif
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
if (argc < 2)
|
|
die("missing argument");
|
|
|
|
// C signature fits C main best anyway
|
|
gst_init(&argc, &argv);
|
|
|
|
std::string url = argv[1];
|
|
// make it URL if not so
|
|
if (!Gst::Uri::is_valid(url)) {
|
|
#if GI_CONFIG_EXCEPTIONS
|
|
try {
|
|
#endif
|
|
url = gi::expect(Gst::filename_to_uri(url));
|
|
#if GI_CONFIG_EXCEPTIONS
|
|
} catch (const GLib::Error &ex) {
|
|
die(ex.what());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
say("Playing " + url);
|
|
|
|
// simply local var will do here
|
|
auto loop = GLib::MainLoop::new_();
|
|
Player player(loop, url);
|
|
|
|
// schedule start
|
|
GLib::idle_add([&] {
|
|
player.start();
|
|
return GLib::SOURCE_REMOVE_;
|
|
});
|
|
// ... and auto end after a while
|
|
GLib::timeout_add_seconds(10, [&] {
|
|
player.stop();
|
|
return GLib::SOURCE_REMOVE_;
|
|
});
|
|
|
|
loop.run();
|
|
}
|