Fix a crash when recording was aborted due to writer error
[skype-call-recorder.git] / call.cpp
blobefd8efdd1b1ef7a73920d89b6399ad588f4da163
1 /*
2 Skype Call Recorder
3 Copyright (C) 2008 jlh (jlh at gmx dot ch)
5 This program is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License as published by the
7 Free Software Foundation; either version 2 of the License, version 3 of
8 the License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 General Public License for more details.
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 The GNU General Public License version 2 is included with the source of
20 this program under the file name COPYING. You can also get a copy on
21 http://www.fsf.org/
24 #include <QStringList>
25 #include <QList>
26 #include <QTcpServer>
27 #include <QTcpSocket>
28 #include <QMessageBox>
29 #include <QHBoxLayout>
30 #include <QVBoxLayout>
31 #include <QPushButton>
32 #include <QCheckBox>
33 #include <QLabel>
34 #include <QDir>
35 #include <QApplication>
36 #include <QStyle>
37 #include <QIcon>
38 #include <ctime>
40 #include "call.h"
41 #include "common.h"
42 #include "skype.h"
43 #include "wavewriter.h"
44 #include "mp3writer.h"
45 #include "preferences.h"
48 Call::Call(Skype *sk, CallID i) :
49 skype(sk),
50 id(i),
51 writer(NULL),
52 isRecording(false)
54 debug(QString("Call %1: Call object contructed").arg(id));
56 // Call objects track calls even before they are in progress and also
57 // when they are not being recorded.
59 // TODO check if we actually should record this call here
60 // and ask if we're unsure
62 skypeName = skype->getObject(QString("CALL %1 PARTNER_HANDLE").arg(id));
63 if (skypeName.isEmpty()) {
64 debug(QString("Call %1: cannot get partner handle").arg(id));
65 skypeName = "UnknownCaller";
68 displayName = skype->getObject(QString("CALL %1 PARTNER_DISPNAME").arg(id));
69 if (displayName.isEmpty()) {
70 debug(QString("Call %1: cannot get partner display name").arg(id));
71 displayName = "Unnamed Caller";
75 Call::~Call() {
76 debug(QString("Call %1: Call object destructed").arg(id));
78 if (isRecording)
79 stopRecording();
81 // QT takes care of deleting servers and sockets
84 namespace {
85 QString escape(const QString &s) {
86 QString out = s;
87 out.replace('%', "%%");
88 out.replace('&', "&&");
89 return out;
93 QString Call::getFileName() const {
94 QString path = preferences.get("output.path").toString();
95 QString fileName = preferences.get("output.pattern").toString();
97 path.replace('~', QDir::homePath());
99 fileName.replace("&s", escape(skypeName));
100 // TODO
101 //fileName.replace("&f", escape(fullName));
102 //fileName.replace("&t", escape(mySkypeName));
103 //fileName.replace("&g", escape(myFullName));
104 fileName.replace("&&", "&");
106 // TODO: uhm, does QT provide any time formatting the strftime() way?
107 char *buf = new char[fileName.size() + 1024];
108 time_t t = std::time(NULL);
109 struct tm *tm = std::localtime(&t);
110 std::strftime(buf, fileName.size() + 1024, fileName.toUtf8().constData(), tm);
111 fileName = buf;
112 delete[] buf;
114 return path + '/' + fileName;
117 void Call::startRecording() {
118 if (isRecording)
119 return;
121 debug(QString("Call %1: start recording").arg(id));
123 // set up encoder for appropriate format
125 QString fileName = getFileName();
127 QString sm = preferences.get("output.channelmode").toString();
129 if (sm == "mono")
130 channelMode = 0;
131 else if (sm == "oerets")
132 channelMode = 2;
133 else /* if (sm == "stereo") */
134 channelMode = 1;
136 QString format = preferences.get("output.format").toString();
138 if (format == "wav")
139 writer = new WaveWriter;
140 else /* if (format == "mp3") */
141 writer = new Mp3Writer;
143 bool b = writer->open(fileName, 16000, channelMode != 0);
145 if (!b) {
146 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
147 QString(PROGRAM_NAME " could not open the file %1. Please verify the output file pattern.").arg(fileName));
148 box->setWindowModality(Qt::NonModal);
149 box->setAttribute(Qt::WA_DeleteOnClose);
150 box->show();
151 writer->remove();
152 delete writer;
153 return;
156 serverLocal = new QTcpServer(this);
157 serverLocal->listen();
158 connect(serverLocal, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
159 serverRemote = new QTcpServer(this);
160 serverRemote->listen();
161 connect(serverRemote, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
163 QString rep1 = skype->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id).arg(serverLocal->serverPort()));
164 QString rep2 = skype->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id).arg(serverRemote->serverPort()));
166 if (!rep1.startsWith("ALTER CALL ") || !rep2.startsWith("ALTER CALL")) {
167 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
168 QString(PROGRAM_NAME " could not obtain the audio streams from Skype and can thus not record this call.\n\n"
169 "The replies from Skype were:\n%1\n%2").arg(rep1).arg(rep2));
170 box->setWindowModality(Qt::NonModal);
171 box->setAttribute(Qt::WA_DeleteOnClose);
172 box->show();
173 writer->remove();
174 delete writer;
175 delete serverRemote;
176 delete serverLocal;
177 return;
180 isRecording = true;
183 void Call::acceptLocal() {
184 socketLocal = serverLocal->nextPendingConnection();
185 serverLocal->close();
186 // we don't delete the server, since it contains the socket.
187 // we could reparent, but that automatic stuff of QT is great
188 connect(socketLocal, SIGNAL(readyRead()), this, SLOT(readLocal()));
189 connect(socketLocal, SIGNAL(disconnected()), this, SLOT(checkConnections()));
192 void Call::acceptRemote() {
193 socketRemote = serverRemote->nextPendingConnection();
194 serverRemote->close();
195 connect(socketRemote, SIGNAL(readyRead()), this, SLOT(readRemote()));
196 connect(socketRemote, SIGNAL(disconnected()), this, SLOT(checkConnections()));
199 void Call::readLocal() {
200 bufferLocal += socketLocal->readAll();
201 if (isRecording)
202 tryToWrite();
205 void Call::readRemote() {
206 bufferRemote += socketRemote->readAll();
207 if (isRecording)
208 tryToWrite();
211 void Call::checkConnections() {
212 if (socketLocal->state() == QAbstractSocket::UnconnectedState && socketRemote->state() == QAbstractSocket::UnconnectedState) {
213 debug(QString("Call %1: both connections closed, stop recording").arg(id));
214 stopRecording();
218 void Call::mixToMono(int samples) {
219 long offset = bufferMono.size();
220 bufferMono.resize(offset + samples * 2);
222 qint16 *monoData = reinterpret_cast<qint16 *>(bufferMono.data()) + offset;
223 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
224 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
226 for (int i = 0; i < samples; i++) {
227 long sum = localData[i] + remoteData[i];
228 if (sum < -32768)
229 sum = -32768;
230 else if (sum > 32767)
231 sum = 32767;
232 monoData[i] = sum;
235 bufferLocal.remove(0, samples * 2);
236 bufferRemote.remove(0, samples * 2);
239 void Call::tryToWrite(bool flush) {
240 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
242 int l = bufferLocal.size();
243 int r = bufferRemote.size();
244 int samples = (l < r ? l : r) / 2;
246 if (!samples && !flush)
247 return;
249 // got new samples to write to file, or have to flush
251 bool success;
253 if (channelMode == 0) {
254 // mono
255 mixToMono(samples);
256 success = writer->write(bufferMono, bufferMono, samples, flush);
257 } else if (channelMode == 1) {
258 // stereo
259 success = writer->write(bufferLocal, bufferRemote, samples, flush);
260 } else if (channelMode == 2) {
261 // oerets
262 success = writer->write(bufferRemote, bufferLocal, samples, flush);
263 } else {
264 success = false;
267 if (!success) {
268 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
269 QString(PROGRAM_NAME " encountered an error while writing this call to disk. Recording terminated."));
270 box->setWindowModality(Qt::NonModal);
271 box->setAttribute(Qt::WA_DeleteOnClose);
272 box->show();
273 stopRecording(false);
274 return;
277 // the writer will remove the samples from the buffers
278 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
280 // TODO: handle the case where the two streams get out of sync (buffers
281 // not equally fulled by a significant amount). does skype document
282 // whether we always get two nice, equal, in-sync streams, even if
283 // there have been transmission errors? perl-script behavior: if out
284 // of sync by more than 6.4ms, then remove 1ms from the stream that's
285 // ahead.
288 void Call::stopRecording(bool flush, bool removeFile) {
289 if (!isRecording)
290 return;
292 debug(QString("Call %1: stop recording").arg(id));
294 // NOTE: we don't delete the sockets here, because we may be here as a
295 // reaction to their disconnected() signals; and they don't like being
296 // deleted during their signals. we don't delete the servers either,
297 // since they own the sockets and we're too lazy to reparent. it's
298 // easiest to let QT handle all this on its own. there will be some
299 // memory wasted if you start/stop recording within the same call a few
300 // times, but unless you do it thousands of times, the waste is more
301 // than acceptable.
303 // flush data to writer
304 if (flush)
305 tryToWrite(true);
306 writer->close();
308 if (removeFile)
309 writer->remove();
311 delete writer;
313 isRecording = false;
317 CallHandler::CallHandler(Skype *s) : skype(s) {
320 void CallHandler::closeAll() {
321 debug("closing all calls");
322 QList<Call *> list = calls.values();
323 for (int i = 0; i < list.size(); i++)
324 list.at(i)->stopRecording();
327 // ---- CallHandler ----
329 void CallHandler::callCmd(const QStringList &args) {
330 CallID id = args.at(0).toInt();
332 if (ignore.contains(id))
333 return;
335 bool newCall = false;
337 if (!calls.contains(id)) {
338 calls[id] = new Call(skype, id);
339 newCall = true;
342 Call *call = calls[id];
344 QString subCmd = args.at(1);
346 if (subCmd == "STATUS") {
347 QString a = args.at(2);
348 if (a == "INPROGRESS")
349 call->startRecording();
350 else if (a == "DURATION") {
351 /* this is where we start recording calls that are already running, for
352 example if the user starts this program after the call has been placed */
353 if (newCall)
354 call->startRecording();
357 // don't stop recording when we get "FINISHED". just wait for
358 // the connections to close so that we really get all the data
362 // ---- RecordConfirmationDialog ----
364 RecordConfirmationDialog::RecordConfirmationDialog(const QString &skypeName, const QString &displayName) {
365 setWindowTitle(PROGRAM_NAME);
366 setAttribute(Qt::WA_DeleteOnClose);
368 QHBoxLayout *bighbox = new QHBoxLayout(this);
369 bighbox->setSizeConstraint(QLayout::SetFixedSize);
371 // get standard icon
372 int iconSize = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize);
373 QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
374 QLabel *iconLabel = new QLabel;
375 iconLabel->setPixmap(icon.pixmap(iconSize, iconSize));
376 bighbox->addWidget(iconLabel, 0, Qt::AlignTop);
378 bighbox->addSpacing(10);
380 QVBoxLayout *vbox = new QVBoxLayout;
381 bighbox->addLayout(vbox);
382 QLabel *label = new QLabel(QString("Do you wish to record this call with <b>%1</b> (%2)?").arg(skypeName).arg(displayName));
383 vbox->addWidget(label);
384 QCheckBox *check = new QCheckBox("Automatically perform this action on the next call with this person");
385 check->setEnabled(false);
386 vbox->addWidget(check);
387 QHBoxLayout *hbox = new QHBoxLayout;
388 QPushButton *button = new QPushButton("Yes, record this call");
389 connect(button, SIGNAL(clicked()), this, SLOT(yesClicked()));
390 hbox->addWidget(button);
391 button = new QPushButton("Do not record this call");
392 connect(button, SIGNAL(clicked()), this, SLOT(noClicked()));
393 hbox->addWidget(button);
394 vbox->addLayout(hbox);
396 connect(this, SIGNAL(rejected()), this, SIGNAL(no()));
398 show();
399 raise();
400 activateWindow();
403 void RecordConfirmationDialog::yesClicked() {
404 emit yes();
405 // TODO update preferences depending on checkbox
406 accept();
409 void RecordConfirmationDialog::noClicked() {
410 emit no();
411 // TODO update preferences depending on checkbox
412 accept();