The sample code below worked for me. The key has been to place the queue behind the wav-parser element and to emit a flushing seek event at the beginning as tpm mentioned already.
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <atomic>
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
#include <gst/app/gstappsrc.h>
// Use seeking mechanism if true, otherwise use EOS mechanism.
constexpr bool doSeek{true};
struct OnNewSampleData
{
GstElement* sinkPipeline;
GstAppSrc* appsrc;
};
static GstFlowReturn on_new_sample_from_sink(GstAppSink* appsink, gpointer user_data)
{
static std::atomic<uint64_t> lastBufferEndTime{0};
// Pass the target appsrc as user_data
auto* data = static_cast<OnNewSampleData*>(user_data);
// Pull the sample from the appsink
GstSample* sample = gst_app_sink_pull_sample(appsink);
if (!sample)
{
return GST_FLOW_OK;
}
// Extract the buffer from the sample
GstBuffer* buffer = gst_sample_get_buffer(sample);
if (buffer)
{
GstCaps* caps = gst_sample_get_caps(sample);
if (caps)
{
// Tell the appsrc exactly what kind of data it is pushing
gst_app_src_set_caps(data->appsrc, caps);
}
// Get the clock from the pipeline. The clock is only available when the pipeline is playing.
GstClock* clock = gst_pipeline_get_pipeline_clock(GST_PIPELINE(data->sinkPipeline));
if (!clock)
{
std::cerr << "Error: Failed to get clock of pipeline.";
return GST_FLOW_OK;
}
GstClockTime runningTime = gst_clock_get_time(clock) - gst_element_get_base_time(data->sinkPipeline);
gst_object_unref(clock);
static constexpr GstClockTime maxBufferDuration{static_cast<GstClockTime>(100e6)};
// Update timestamp for next cycle
if (!GST_BUFFER_DURATION_IS_VALID(buffer))
{
std::cerr << ": Ignore buffer: Invalid buffer duration.";
return GST_FLOW_OK;
}
const auto bufferDuration = GST_BUFFER_DURATION(buffer);
if (bufferDuration > maxBufferDuration)
{
std::cerr << ": Ignore buffer: Buffer duration (" << GST_TIME_AS_MSECONDS(bufferDuration)
<< " ms) too high and unreliable.";
return GST_FLOW_OK;
}
// The minimum delay of a buffer in nanoseconds. If the buffer PTS is less than this delay there is a
// significant risk that the buffer is dropped as the buffer has to reach the pipelines sink
// element by the time the pipeline clock aligns with the current running-time accounting for all
// intermediate processing steps.
constexpr GstClockTime minDelay{static_cast<GstClockTime>(1e6)};
// For hysteresis this maximum delay in ns is higher than the minimum delay above.
constexpr GstClockTime maxDelay{static_cast<GstClockTime>(5e6)};
GstClockTime assignedPTS{0U};
uint64_t expected = lastBufferEndTime.load(std::memory_order_relaxed);
while (true)
{
const auto observed = expected;
auto candidate = observed;
// If the last timestamp is too old relative to the running clock
// because the source pipeline has been PAUSED or the processing can not keep up
// reset to runningTime + maxDelay
if (observed < (runningTime + minDelay))
{
candidate = static_cast<uint64_t>(runningTime + maxDelay);
std::cerr << ": lastBufferEndTime (" << GST_TIME_AS_MSECONDS(observed)
<< " ms) is too old -> reset to " << GST_TIME_AS_MSECONDS(candidate) << " ms";
}
assignedPTS = candidate;
uint64_t desired = candidate + static_cast<uint64_t>(bufferDuration);
// Try to atomically replace expected (the value we observed) with desired.
// On success the atomic was still equal expected and now equals desired.
// On failure: the atomic has been changed in the meantime and does not equal expected.
// Expected is updated with the current atomic value and we loop.
if (lastBufferEndTime.compare_exchange_weak(expected, desired, std::memory_order_acq_rel,
std::memory_order_relaxed))
{
break; // success: assignedPTS is valid for this buffer
}
}
// Set new timestamp to current buffer
GST_BUFFER_PTS(buffer) = assignedPTS;
// Push the buffer to the appsrc element. Be aware: appsrc takes ownership of buffer!
GstBuffer* push_buffer = gst_buffer_make_writable(gst_buffer_ref(buffer));
GstFlowReturn ret = gst_app_src_push_buffer(data->appsrc, push_buffer);
if (ret != GST_FLOW_OK)
{
// we do not have to unref the buffer here because push_buffer has been called previously
std::cerr << "Failed to push buffer to appsrc: ";
}
}
// Properly release the sample container
gst_sample_unref(sample);
return GST_FLOW_OK;
}
static GstBusSyncReply busCallback(GstBus* bus, GstMessage* message, gpointer data)
{
std::string logMessage = "[BUS MESSAGE]";
GError* err = nullptr;
gchar* debug_info = nullptr;
switch (GST_MESSAGE_TYPE(message))
{
case GST_MESSAGE_ERROR:
{
gst_message_parse_error(message, &err, &debug_info);
logMessage += "Error from pipeline: " + std::string(err->message);
if (debug_info)
{
logMessage += "\n (Debug info: " + std::string(debug_info) + ")\n";
std::cerr << logMessage;
}
g_error_free(err);
g_free(debug_info);
break;
}
case GST_MESSAGE_WARNING:
{
gst_message_parse_warning(message, &err, &debug_info);
logMessage += "Warning from pipeline: " + std::string(err->message);
if (debug_info)
{
logMessage += "\n (Debug info: " + std::string(debug_info) + ")\n";
std::cout << logMessage;
}
g_error_free(err);
g_free(debug_info);
break;
}
case GST_MESSAGE_EOS:
{
logMessage += "End-Of-Stream reached in pipeline: ";
std::cout << logMessage << std::endl;
GstElement* pipeline = GST_ELEMENT(data);
if (pipeline)
{
// Dispatch fire-and-forget thread to execute the seek safely outside the streaming context
std::thread(
[pipeline]()
{
// std::this_thread::sleep_for(std::chrono::milliseconds(1000));
gint64 dur;
if (!gst_element_query_duration(pipeline, GST_FORMAT_TIME, &dur))
std::cout << "Failed to query duration!\n";
gboolean isSeeked =
gst_element_seek(pipeline, 1.0, GST_FORMAT_TIME,
static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE),
GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, dur);
if (!isSeeked)
{
std::cout << "Failed to seek to start asynchronously.\n";
}
else
{
std::cout << "Loop flushing seek successful.\n";
}
})
.detach();
}
break;
}
case GST_MESSAGE_SEGMENT_DONE:
{
std::cout << "Segment DONE.\n";
GstElement* pipeline = GST_ELEMENT(data);
if (pipeline)
{
// Dispatch fire-and-forget thread to execute the seek safely outside the streaming context
std::thread(
[pipeline]()
{
// std::this_thread::sleep_for(std::chrono::milliseconds(1000)); error
// Re-query duration if needed, or stick to your layout:
gint64 dur = GST_CLOCK_TIME_NONE;
gst_element_query_duration(pipeline, GST_FORMAT_TIME, &dur);
gboolean isSeeked =
gst_element_seek(pipeline, 1.0, GST_FORMAT_TIME,
static_cast<GstSeekFlags>(GST_SEEK_FLAG_SEGMENT | GST_SEEK_FLAG_ACCURATE),
GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE);
if (!isSeeked)
{
std::cout << "Failed to seek to start asynchronously.\n";
}
else
{
std::cout << "Loop seek successful.\n";
}
})
.detach();
}
break;
}
default:
break;
}
gst_message_unref(message);
return GST_BUS_DROP; // Do not forward to async handler -> unnecessary
}
int main()
{
gst_init(nullptr, nullptr);
GstElement* srcPipeline = gst_pipeline_new("audio-player");
GstBus* bus = gst_element_get_bus(srcPipeline);
GstElement* appsink = gst_element_factory_make("appsink", "appsink");
{
GstElement* source = gst_element_factory_make("filesrc", "file-source");
GstElement* queue = gst_element_factory_make("queue", "queue");
GstElement* parser = gst_element_factory_make("wavparse", "wav-parser");
GstElement* convert = gst_element_factory_make("audioconvert", "audio-converter");
GstElement* resample = gst_element_factory_make("audioresample", "audio-resampler");
g_object_set(G_OBJECT(appsink), "async", true, "sync", true, nullptr);
gst_app_sink_set_max_buffers(GST_APP_SINK(appsink), 2);
// drop old samples
gst_app_sink_set_drop(GST_APP_SINK(appsink), true);
// The GstAppSink has to emit the "new-sample" signal
gst_app_sink_set_emit_signals(GST_APP_SINK(appsink), true);
if (!srcPipeline || !source || !queue|| !parser || !convert || !resample || !appsink)
{
std::cerr << "Not all elements could be created.\n";
return -1;
}
g_object_set(G_OBJECT(source), "location", "D:\\ring.wav", nullptr);
gst_bus_set_sync_handler(bus, busCallback, srcPipeline, nullptr);
gst_bin_add_many(GST_BIN(srcPipeline), source,queue, parser, convert, resample, appsink, nullptr);
if (!gst_element_link_many(source, parser,queue, convert, resample, appsink, nullptr))
{
std::cerr << "Link failure.\n";
return -1;
}
// Preroll
std::cout << "Preroll...\n";
if (auto ret = gst_element_set_state(srcPipeline, GST_STATE_PAUSED); ret == GST_STATE_CHANGE_FAILURE)
{
std::cerr << "Unable to set the pipeline to the GST_STATE_PAUSED state.\n";
gst_object_unref(bus);
gst_object_unref(srcPipeline);
return -1;
}
// Explicitly block until the pipeline completes its asynchronous transition to PAUSED
// This fills the internal audio buffers and evaluates file properties.
if (auto ret = gst_element_get_state(srcPipeline, nullptr, nullptr, GST_CLOCK_TIME_NONE);
ret == GST_STATE_CHANGE_FAILURE)
{
std::cerr << "Pipeline failed during preroll validation.\n";
return -1;
}
if (doSeek)
{
// Seek at startup
std::cout << "Seek at startup...\n";
gint64 dur;
if (!gst_element_query_duration(srcPipeline, GST_FORMAT_TIME, &dur))
std::cout << "Failed to query duration!\n";
if (auto isSeeked = gst_element_seek(
srcPipeline, 1.0, GST_FORMAT_TIME,
static_cast<GstSeekFlags>(GST_SEEK_FLAG_SEGMENT | GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE),
GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE);
!isSeeked)
std::cout << "Failed to seek to start at startup.\n";
}
}
GstElement* sinkPipeline = gst_pipeline_new("audio-player2");
GstElement* appsrc = gst_element_factory_make("appsrc", "appsrc");
{
gst_app_src_set_emit_signals(GST_APP_SRC(appsrc), false);
g_object_set(G_OBJECT(appsrc), "format", GST_FORMAT_TIME, "is-live", true, "do-timestamp", false,
"min-latency", 0, "max-latency", 5e6, "max-time", 5e6, "stream-type",
GST_APP_STREAM_TYPE_STREAM, "leaky-type", GST_APP_LEAKY_TYPE_DOWNSTREAM, nullptr);
GstElement* convert = gst_element_factory_make("audioconvert", "audio-converter2");
GstElement* resample = gst_element_factory_make("audioresample", "audio-resampler2");
GstElement* sink = gst_element_factory_make("wasapi2sink", "wasapi-sink");
if (!sinkPipeline || !appsrc || !convert || !resample || !sink)
{
std::cerr << "Not all elements could be created.\n";
return -1;
}
gst_bin_add_many(GST_BIN(sinkPipeline), appsrc, convert, resample, sink, nullptr);
if (!gst_element_link_many(appsrc, convert, resample, sink, nullptr))
{
std::cerr << "Link failure.\n";
return -1;
}
// Start playing
std::cout << "Starting playback...\n";
if (auto ret = gst_element_set_state(sinkPipeline, GST_STATE_PLAYING); ret == GST_STATE_CHANGE_FAILURE)
{
std::cerr << "Unable to set the pipeline to the playing state.\n";
gst_object_unref(sinkPipeline);
return -1;
}
}
OnNewSampleData data{sinkPipeline, GST_APP_SRC(appsrc)};
g_signal_connect(GST_APP_SINK(appsink), "new-sample", G_CALLBACK(on_new_sample_from_sink), &data);
// Start playing
std::cout << "Starting playback...\n";
if (auto ret = gst_element_set_state(srcPipeline, GST_STATE_PLAYING); ret == GST_STATE_CHANGE_FAILURE)
{
std::cerr << "Unable to set the pipeline to the playing state.\n";
gst_object_unref(bus);
gst_object_unref(srcPipeline);
return -1;
}
// Wait until error or EOS (End of Stream)
GstMessage* msg = gst_bus_timed_pop_filtered(
bus, GST_CLOCK_TIME_NONE, static_cast<GstMessageType>(GST_MESSAGE_ERROR | GST_MESSAGE_EOS));
// Free resources
gst_element_set_state(srcPipeline, GST_STATE_NULL);
gst_element_set_state(sinkPipeline, GST_STATE_NULL);
gst_object_unref(bus);
gst_object_unref(srcPipeline);
gst_object_unref(sinkPipeline);
std::cout << "done\n";
return 0;
}