|
|
|
/***************************************************************************
|
|
|
|
timeshifter.cpp - description
|
|
|
|
-------------------
|
|
|
|
begin : Mon May 16 13:39:31 CEST 2005
|
|
|
|
copyright : (C) 2005 by Ernst Martin Witte
|
|
|
|
email : witte@kawo1.rwth-aachen.de
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
|
|
* *
|
|
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
|
|
* it under the terms of the GNU General Public License as published by *
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
|
|
* (at your option) any later version. *
|
|
|
|
* *
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include <config.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <tdelocale.h>
|
|
|
|
#include <linux/soundcard.h>
|
|
|
|
|
|
|
|
#include "../../src/include/utils.h"
|
|
|
|
#include "timeshifter.h"
|
|
|
|
#include "timeshifter-configuration.h"
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
PLUGIN_LIBRARY_FUNCTIONS(TimeShifter, "tderadio-timeshifter", i18n("TimeShift Support"));
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
TimeShifter::TimeShifter (const TQString &name)
|
|
|
|
: PluginBase(name, i18n("TimeShifter Plugin")),
|
|
|
|
m_TempFileName("/tmp/tderadio-timeshifter-tempfile"),
|
|
|
|
m_TempFileMaxSize(256*1024*1024),
|
|
|
|
m_PlaybackMixerID(TQString()),
|
|
|
|
m_PlaybackMixerChannel("PCM"),
|
|
|
|
m_orgVolume(0.0),
|
|
|
|
m_PlaybackMetaData(0,0,0),
|
|
|
|
m_PlaybackDataLeftInBuffer(0),
|
|
|
|
m_RingBuffer(m_TempFileName, m_TempFileMaxSize)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TimeShifter::~TimeShifter ()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::connectI (Interface *i)
|
|
|
|
{
|
|
|
|
bool a = PluginBase::connectI(i);
|
|
|
|
bool b = ISoundStreamClient::connectI(i);
|
|
|
|
return a || b;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::disconnectI (Interface *i)
|
|
|
|
{
|
|
|
|
bool a = PluginBase::disconnectI(i);
|
|
|
|
bool b = ISoundStreamClient::disconnectI(i);
|
|
|
|
return a || b;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TimeShifter::noticeConnectedI (ISoundStreamServer *s, bool pointer_valid)
|
|
|
|
{
|
|
|
|
ISoundStreamClient::noticeConnectedI(s, pointer_valid);
|
|
|
|
if (s && pointer_valid) {
|
|
|
|
s->register4_notifySoundStreamClosed(this);
|
|
|
|
s->register4_sendStartPlayback(this);
|
|
|
|
s->register4_sendStopPlayback(this);
|
|
|
|
s->register4_sendPausePlayback(this);
|
|
|
|
s->register4_notifySoundStreamData(this);
|
|
|
|
s->register4_notifyReadyForPlaybackData(this);
|
|
|
|
s->register4_querySoundStreamDescription(this);
|
|
|
|
s->register4_sendStartCaptureWithFormat(this);
|
|
|
|
s->register4_sendStopCapture(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TimeShifter::saveState (TDEConfig *config) const
|
|
|
|
{
|
|
|
|
config->setGroup(TQString("timeshifter-") + name());
|
|
|
|
|
|
|
|
config->writeEntry("temp-file-name", m_TempFileName);
|
|
|
|
config->writeEntry("max-file-size", m_TempFileMaxSize / 1024 / 1024);
|
|
|
|
|
|
|
|
config->writeEntry("PlaybackMixerID", m_PlaybackMixerID);
|
|
|
|
config->writeEntry("PlaybackMixerChannel", m_PlaybackMixerChannel);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TimeShifter::restoreState (TDEConfig *config)
|
|
|
|
{
|
|
|
|
config->setGroup(TQString("timeshifter-") + name());
|
|
|
|
|
|
|
|
TQString fname = config->readEntry("temp-file-name", "/tmp/tderadio-timeshifter-tempfile");
|
|
|
|
TQ_UINT64 fsize = 1024 * 1024 * config->readNumEntry("max-file-size", 256);
|
|
|
|
|
|
|
|
TQString mixerID = config->readEntry ("PlaybackMixerID", TQString());
|
|
|
|
TQString channel = config->readEntry ("PlaybackMixerChannel", "PCM");
|
|
|
|
|
|
|
|
setPlaybackMixer(mixerID, channel);
|
|
|
|
setTempFile(fname, fsize);
|
|
|
|
|
|
|
|
emit sigUpdateConfig();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ConfigPageInfo TimeShifter::createConfigurationPage()
|
|
|
|
{
|
|
|
|
TimeShifterConfiguration *conf = new TimeShifterConfiguration(NULL, this);
|
|
|
|
TQObject::connect(this, TQT_SIGNAL(sigUpdateConfig()), conf, TQT_SLOT(slotUpdateConfig()));
|
|
|
|
return ConfigPageInfo (conf,
|
|
|
|
i18n("Timeshifter"),
|
|
|
|
i18n("Timeshifter Options"),
|
|
|
|
"tderadio_pause");
|
|
|
|
}
|
|
|
|
|
|
|
|
AboutPageInfo TimeShifter::createAboutPage()
|
|
|
|
{
|
|
|
|
return AboutPageInfo();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::noticeSoundStreamClosed(SoundStreamID id)
|
|
|
|
{
|
|
|
|
return stopPlayback(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TimeShifter::startPlayback(SoundStreamID id)
|
|
|
|
{
|
|
|
|
if (id == m_OrgStreamID) {
|
|
|
|
m_StreamPaused = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TimeShifter::stopPlayback(SoundStreamID id)
|
|
|
|
{
|
|
|
|
if (id == m_NewStreamID) {
|
|
|
|
|
|
|
|
return sendStopPlayback(m_OrgStreamID);
|
|
|
|
|
|
|
|
} else if (id == m_OrgStreamID) {
|
|
|
|
|
|
|
|
SoundStreamID tmp_newID = m_NewStreamID;
|
|
|
|
SoundStreamID tmp_orgID = m_OrgStreamID;
|
|
|
|
|
|
|
|
m_OrgStreamID.invalidate();
|
|
|
|
m_NewStreamID.invalidate();
|
|
|
|
|
|
|
|
sendStopCapture(tmp_newID);
|
|
|
|
closeSoundStream(tmp_newID);
|
|
|
|
stopPlayback(tmp_newID);
|
|
|
|
m_RingBuffer.clear();
|
|
|
|
m_PlaybackMetaData = SoundMetaData(0,0,0);
|
|
|
|
m_PlaybackDataLeftInBuffer = 0;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::pausePlayback(SoundStreamID id)
|
|
|
|
{
|
|
|
|
if (!m_OrgStreamID.isValid()) {
|
|
|
|
SoundStreamID orgid = id;
|
|
|
|
SoundStreamID newid = createNewSoundStream(orgid, false);
|
|
|
|
m_OrgStreamID = orgid;
|
|
|
|
m_NewStreamID = newid;
|
|
|
|
notifySoundStreamCreated(newid);
|
|
|
|
notifySoundStreamRedirected(orgid, newid);
|
|
|
|
queryPlaybackVolume(newid, m_orgVolume);
|
|
|
|
sendMute(newid);
|
|
|
|
sendPlaybackVolume(newid, 0);
|
|
|
|
|
|
|
|
m_NewStreamID.invalidate();
|
|
|
|
sendStopPlayback(newid);
|
|
|
|
m_NewStreamID = newid;
|
|
|
|
|
|
|
|
m_StreamPaused = true;
|
|
|
|
|
|
|
|
m_RingBuffer.clear();
|
|
|
|
m_PlaybackMetaData = SoundMetaData(0,0,0);
|
|
|
|
m_PlaybackDataLeftInBuffer = 0;
|
|
|
|
|
|
|
|
sendStartCaptureWithFormat(m_NewStreamID, m_SoundFormat, m_realSoundFormat);
|
|
|
|
|
|
|
|
ISoundStreamClient *playback_mixer = searchPlaybackMixer();
|
|
|
|
if (playback_mixer) {
|
|
|
|
playback_mixer->preparePlayback(m_OrgStreamID, m_PlaybackMixerChannel, /*active*/true, /*startimmediately*/ true);
|
|
|
|
m_PlaybackMixerID = playback_mixer->getSoundStreamClientID();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else if (id == m_OrgStreamID) {
|
|
|
|
m_StreamPaused = !m_StreamPaused;
|
|
|
|
if (!m_StreamPaused) {
|
|
|
|
// sendStartPlayback(m_OrgStreamID);
|
|
|
|
sendUnmute(m_OrgStreamID);
|
|
|
|
sendPlaybackVolume(m_OrgStreamID, m_orgVolume);
|
|
|
|
} else {
|
|
|
|
queryPlaybackVolume(m_OrgStreamID, m_orgVolume);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
size_t TimeShifter::writeMetaDataToBuffer(const SoundMetaData &md, char *buffer, size_t buffer_size)
|
|
|
|
{
|
|
|
|
TQ_UINT64 pos = md.position();
|
|
|
|
time_t abs = md.absoluteTimestamp();
|
|
|
|
time_t rel = md.relativeTimestamp();
|
|
|
|
size_t url_len = md.url().url().length() + 1;
|
|
|
|
size_t req_size = sizeof(req_size) + sizeof(pos) + sizeof(abs) + sizeof(rel) + sizeof(url_len) + url_len;
|
|
|
|
if (req_size <= buffer_size) {
|
|
|
|
*(size_t*)buffer = req_size;
|
|
|
|
buffer += sizeof(req_size);
|
|
|
|
*(TQ_UINT64*)buffer = pos;
|
|
|
|
buffer += sizeof(pos);
|
|
|
|
*(time_t*)buffer = abs;
|
|
|
|
buffer += sizeof(abs);
|
|
|
|
*(time_t*)buffer = rel;
|
|
|
|
buffer += sizeof(rel);
|
|
|
|
*(size_t*)buffer = url_len;
|
|
|
|
buffer += sizeof(url_len);
|
|
|
|
memcpy(buffer, md.url().url().ascii(), url_len);
|
|
|
|
buffer += url_len;
|
|
|
|
return req_size;
|
|
|
|
} else if (buffer_size >= sizeof(req_size)) {
|
|
|
|
*(size_t*)buffer = sizeof(req_size);
|
|
|
|
return sizeof(req_size);
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t TimeShifter::readMetaDataFromBuffer(SoundMetaData &md, const char *buffer, size_t buffer_size)
|
|
|
|
{
|
|
|
|
size_t req_size = 0;
|
|
|
|
TQ_UINT64 pos = 0;
|
|
|
|
time_t abs = 0;
|
|
|
|
time_t rel = 0;
|
|
|
|
size_t url_len = 0;
|
|
|
|
KURL url;
|
|
|
|
if (buffer_size >= sizeof(req_size)) {
|
|
|
|
req_size = *(size_t*)buffer;
|
|
|
|
buffer += sizeof(req_size);
|
|
|
|
if (req_size > sizeof(req_size)) {
|
|
|
|
pos = *(TQ_UINT64*)buffer;
|
|
|
|
buffer += sizeof(TQ_UINT64);
|
|
|
|
abs = *(time_t*)buffer;
|
|
|
|
buffer += sizeof(abs);
|
|
|
|
rel = *(time_t*)buffer;
|
|
|
|
buffer += sizeof(rel);
|
|
|
|
url_len = *(size_t*)buffer;
|
|
|
|
buffer += sizeof(url_len);
|
|
|
|
url = buffer;
|
|
|
|
buffer += url_len;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
md = SoundMetaData(pos, rel, abs, url);
|
|
|
|
return req_size;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::noticeSoundStreamData(SoundStreamID id, const SoundFormat &/*sf*/, const char *data, size_t size, size_t &consumed_size, const SoundMetaData &md)
|
|
|
|
{
|
|
|
|
if (id == m_NewStreamID) {
|
|
|
|
char buffer_meta[1024];
|
|
|
|
size_t meta_buffer_size = writeMetaDataToBuffer(md, buffer_meta, 1024);
|
|
|
|
size_t packet_size = meta_buffer_size + sizeof(size) + size;
|
|
|
|
if (packet_size > m_RingBuffer.getMaxSize())
|
|
|
|
return false;
|
|
|
|
TQ_INT64 diff = m_RingBuffer.getFreeSize() - packet_size;
|
|
|
|
while (diff < 0) {
|
|
|
|
skipPacketInRingBuffer();
|
|
|
|
diff = m_RingBuffer.getFreeSize() - packet_size;
|
|
|
|
}
|
|
|
|
m_RingBuffer.addData(buffer_meta, meta_buffer_size);
|
|
|
|
m_RingBuffer.addData((const char*)&size, sizeof(size));
|
|
|
|
m_RingBuffer.addData(data, size);
|
|
|
|
consumed_size = (consumed_size == SIZE_T_DONT_CARE) ? size : min(consumed_size, size);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TimeShifter::skipPacketInRingBuffer()
|
|
|
|
{
|
|
|
|
if (m_PlaybackDataLeftInBuffer > 0) {
|
|
|
|
m_RingBuffer.removeData(m_PlaybackDataLeftInBuffer);
|
|
|
|
} else {
|
|
|
|
size_t meta_size = 0;
|
|
|
|
m_RingBuffer.takeData((char*)&meta_size, sizeof(meta_size));
|
|
|
|
m_RingBuffer.removeData(meta_size - sizeof(meta_size));
|
|
|
|
size_t packet_size = 0;
|
|
|
|
m_RingBuffer.takeData((char*)&packet_size, sizeof(packet_size));
|
|
|
|
m_RingBuffer.removeData(packet_size - sizeof(packet_size));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::noticeReadyForPlaybackData(SoundStreamID id, size_t free_size)
|
|
|
|
{
|
|
|
|
if (id == m_OrgStreamID && !m_StreamPaused) {
|
|
|
|
|
|
|
|
while (!m_RingBuffer.error() && m_RingBuffer.getFillSize() > 0 && free_size > 0) {
|
|
|
|
if (m_PlaybackDataLeftInBuffer == 0) {
|
|
|
|
char meta_buffer[1024];
|
|
|
|
size_t &meta_size = *(size_t*)meta_buffer;
|
|
|
|
meta_size = 0;
|
|
|
|
m_RingBuffer.takeData(meta_buffer, sizeof(meta_size));
|
|
|
|
if (meta_size && meta_size <= 1024) {
|
|
|
|
m_RingBuffer.takeData(meta_buffer + sizeof(meta_size), meta_size - sizeof(meta_size));
|
|
|
|
readMetaDataFromBuffer(m_PlaybackMetaData, meta_buffer, meta_size);
|
|
|
|
} else {
|
|
|
|
m_RingBuffer.removeData(meta_size - sizeof(meta_size));
|
|
|
|
}
|
|
|
|
|
|
|
|
m_PlaybackDataLeftInBuffer = 0;
|
|
|
|
m_RingBuffer.takeData((char*)&m_PlaybackDataLeftInBuffer, sizeof(m_PlaybackDataLeftInBuffer));
|
|
|
|
}
|
|
|
|
|
|
|
|
const size_t buffer_size = 65536;
|
|
|
|
char buffer[buffer_size];
|
|
|
|
|
|
|
|
while (!m_RingBuffer.error() && m_PlaybackDataLeftInBuffer > 0 && free_size > 0) {
|
|
|
|
size_t s = m_PlaybackDataLeftInBuffer < free_size ? m_PlaybackDataLeftInBuffer : free_size;
|
|
|
|
|
|
|
|
if (s > buffer_size)
|
|
|
|
s = buffer_size;
|
|
|
|
s = m_RingBuffer.takeData(buffer, s);
|
|
|
|
|
|
|
|
size_t consumed_size = SIZE_T_DONT_CARE;
|
|
|
|
notifySoundStreamData(m_OrgStreamID, m_realSoundFormat, buffer, s, consumed_size, m_PlaybackMetaData);
|
|
|
|
if (consumed_size == SIZE_T_DONT_CARE)
|
|
|
|
consumed_size = s;
|
|
|
|
|
|
|
|
free_size -= consumed_size;
|
|
|
|
m_PlaybackDataLeftInBuffer -= consumed_size;
|
|
|
|
if (consumed_size < s) {
|
|
|
|
logError(i18n("TimeShifter::notifySoundStreamData: clients skipped %1 bytes. Data Lost").arg(s - consumed_size));
|
|
|
|
free_size = 0; // break condition for outer loop
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ISoundStreamClient *TimeShifter::searchPlaybackMixer()
|
|
|
|
{
|
|
|
|
ISoundStreamClient *playback_mixer = getSoundStreamClientWithID(m_PlaybackMixerID);
|
|
|
|
|
|
|
|
// some simple sort of autodetection if one mixer isn't present any more
|
|
|
|
if (!playback_mixer) {
|
|
|
|
TQPtrList<ISoundStreamClient> playback_mixers = queryPlaybackMixers();
|
|
|
|
if (!playback_mixers.isEmpty())
|
|
|
|
playback_mixer = playback_mixers.first();
|
|
|
|
}
|
|
|
|
return playback_mixer;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TimeShifter::setPlaybackMixer(const TQString &soundStreamClientID, const TQString &ch)
|
|
|
|
{
|
|
|
|
m_PlaybackMixerID = soundStreamClientID;
|
|
|
|
m_PlaybackMixerChannel = ch;
|
|
|
|
|
|
|
|
ISoundStreamClient *playback_mixer = searchPlaybackMixer();
|
|
|
|
|
|
|
|
float oldVolume;
|
|
|
|
if (m_OrgStreamID.isValid()) {
|
|
|
|
queryPlaybackVolume(m_OrgStreamID, oldVolume);
|
|
|
|
sendStopPlayback(m_OrgStreamID);
|
|
|
|
sendReleasePlayback(m_OrgStreamID);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (playback_mixer)
|
|
|
|
playback_mixer->preparePlayback(m_OrgStreamID, m_PlaybackMixerChannel, /*active*/true, /*start_imm*/false);
|
|
|
|
|
|
|
|
if (m_OrgStreamID.isValid()) {
|
|
|
|
sendStartPlayback(m_OrgStreamID);
|
|
|
|
sendPlaybackVolume(m_OrgStreamID, oldVolume);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TimeShifter::setTempFile(const TQString &filename, TQ_UINT64 s)
|
|
|
|
{
|
|
|
|
m_RingBuffer.clear();
|
|
|
|
m_RingBuffer.resize(m_TempFileName = filename, m_TempFileMaxSize = s);
|
|
|
|
m_PlaybackMetaData = SoundMetaData(0,0,0, i18n("internal stream, not stored"));
|
|
|
|
m_PlaybackDataLeftInBuffer = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TimeShifter::getSoundStreamDescription(SoundStreamID id, TQString &descr) const
|
|
|
|
{
|
|
|
|
if (id == m_NewStreamID) {
|
|
|
|
descr = name();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TimeShifter::startCaptureWithFormat(
|
|
|
|
SoundStreamID id,
|
|
|
|
const SoundFormat &proposed_format,
|
|
|
|
SoundFormat &real_format,
|
|
|
|
bool force_format
|
|
|
|
)
|
|
|
|
{
|
|
|
|
if (id == m_OrgStreamID) {
|
|
|
|
if (force_format && m_realSoundFormat != proposed_format) {
|
|
|
|
sendStopCapture(m_NewStreamID);
|
|
|
|
sendStartCaptureWithFormat(m_NewStreamID, proposed_format, m_realSoundFormat);
|
|
|
|
}
|
|
|
|
real_format = m_realSoundFormat;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TimeShifter::stopCapture(SoundStreamID id)
|
|
|
|
{
|
|
|
|
if (id == m_OrgStreamID) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#include "timeshifter.moc"
|