Add list add/remove/contains methods to Preference
[skype-call-recorder.git] / call.cpp
blob5d196fb25e794e3a5d56f0d90b80a20bba2f46f9
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 <ctime>
31 #include "call.h"
32 #include "common.h"
33 #include "skype.h"
34 #include "wavewriter.h"
35 #include "mp3writer.h"
36 #include "preferences.h"
37 #include "callgui.h"
40 Call::Call(QObject *p, Skype *sk, CallID i) :
41 QObject(p),
42 skype(sk),
43 id(i),
44 status("UNKNOWN"),
45 writer(NULL),
46 isRecording(false),
47 shouldRecord(1),
48 confirmation(NULL)
50 debug(QString("Call %1: Call object contructed").arg(id));
52 // Call objects track calls even before they are in progress and also
53 // when they are not being recorded.
55 // TODO check if we actually should record this call here
56 // and ask if we're unsure
58 skypeName = skype->getObject(QString("CALL %1 PARTNER_HANDLE").arg(id));
59 if (skypeName.isEmpty()) {
60 debug(QString("Call %1: cannot get partner handle").arg(id));
61 skypeName = "UnknownCaller";
64 displayName = skype->getObject(QString("CALL %1 PARTNER_DISPNAME").arg(id));
65 if (displayName.isEmpty()) {
66 debug(QString("Call %1: cannot get partner display name").arg(id));
67 displayName = "Unnamed Caller";
71 Call::~Call() {
72 debug(QString("Call %1: Call object destructed").arg(id));
74 if (isRecording)
75 stopRecording();
77 delete confirmation;
79 setStatus("UNKNOWN");
81 // QT takes care of deleting servers and sockets
84 bool Call::okToDelete() const {
85 // this is used for checking whether past calls may now be deleted.
86 // when a past call hasn't been decided yet whether it should have been
87 // recorded, then it may not be deleted until the decision has been
88 // made by the user.
90 if (isRecording)
91 return false;
93 if (confirmation)
94 /* confirmation dialog still open */
95 return false;
97 return true;
100 void Call::setStatus(const QString &s) {
101 bool wasInProgress = status == "INPROGRESS";
102 status = s;
103 bool nowInProgress = status == "INPROGRESS";
105 if (!wasInProgress && nowInProgress)
106 emit startedCall(skypeName);
107 else if (wasInProgress && !nowInProgress)
108 emit stoppedCall();
111 namespace {
112 QString escape(const QString &s) {
113 QString out = s;
114 out.replace('%', "%%");
115 out.replace('&', "&&");
116 return out;
120 QString Call::constructFileName() const {
121 QString path = getOutputPath();
122 QString fileName = preferences.get("output.pattern").toString();
124 fileName.replace("&s", escape(skypeName));
125 // TODO
126 //fileName.replace("&f", escape(fullName));
127 //fileName.replace("&t", escape(mySkypeName));
128 //fileName.replace("&g", escape(myFullName));
129 fileName.replace("&&", "&");
131 // TODO: uhm, does QT provide any time formatting the strftime() way?
132 char *buf = new char[fileName.size() + 1024];
133 time_t t = std::time(NULL);
134 struct tm *tm = std::localtime(&t);
135 std::strftime(buf, fileName.size() + 1024, fileName.toUtf8().constData(), tm);
136 fileName = buf;
137 delete[] buf;
139 return path + '/' + fileName;
142 void Call::setShouldRecord() {
143 // this sets shouldRecord based on preferences. shouldRecord is 0 if
144 // the call should not be recorded, 1 if we should ask and 2 if we
145 // should record
147 QStringList list = preferences.get("autorecord.yes").toList();
148 if (list.contains(skypeName)) {
149 shouldRecord = 2;
150 return;
153 list = preferences.get("autorecord.ask").toList();
154 if (list.contains(skypeName)) {
155 shouldRecord = 1;
156 return;
159 list = preferences.get("autorecord.no").toList();
160 if (list.contains(skypeName)) {
161 shouldRecord = 0;
162 return;
165 QString def = preferences.get("autorecord.default").toString();
166 if (def == "yes")
167 shouldRecord = 2;
168 else if (def == "ask")
169 shouldRecord = 1;
170 else if (def == "no")
171 shouldRecord = 0;
172 else
173 shouldRecord = 1;
176 void Call::ask() {
177 confirmation = new RecordConfirmationDialog(skypeName, displayName);
178 connect(confirmation, SIGNAL(yes()), this, SLOT(confirmRecording()));
179 connect(confirmation, SIGNAL(no()), this, SLOT(denyRecording()));
182 void Call::hideConfirmation(int should) {
183 if (!confirmation)
184 return;
185 delete confirmation;
186 confirmation = NULL;
187 shouldRecord = should;
190 void Call::confirmRecording() {
191 shouldRecord = 2;
192 confirmation = NULL;
195 void Call::denyRecording() {
196 // note that the call might already be finished by now
197 shouldRecord = 0;
198 confirmation = NULL;
199 stopRecording(true);
200 removeFile();
203 void Call::removeFile() {
204 debug(QString("Removing '%1'").arg(fileName));
205 QFile::remove(fileName);
208 void Call::startRecording(bool force) {
209 if (force)
210 hideConfirmation(2);
212 if (isRecording)
213 return;
215 if (!force) {
216 setShouldRecord();
217 if (shouldRecord == 0)
218 return;
219 if (shouldRecord == 1)
220 ask();
223 debug(QString("Call %1: start recording").arg(id));
225 // set up encoder for appropriate format
227 QString fn = constructFileName();
229 QString sm = preferences.get("output.channelmode").toString();
231 if (sm == "mono")
232 channelMode = 0;
233 else if (sm == "oerets")
234 channelMode = 2;
235 else /* if (sm == "stereo") */
236 channelMode = 1;
238 QString format = preferences.get("output.format").toString();
240 if (format == "wav")
241 writer = new WaveWriter;
242 else /* if (format == "mp3") */
243 writer = new Mp3Writer;
245 bool b = writer->open(fn, 16000, channelMode != 0);
246 fileName = writer->fileName();
248 if (!b) {
249 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
250 QString(PROGRAM_NAME " could not open the file %1. Please verify the output file pattern.").arg(fileName));
251 box->setWindowModality(Qt::NonModal);
252 box->setAttribute(Qt::WA_DeleteOnClose);
253 box->show();
254 removeFile();
255 delete writer;
256 return;
259 serverLocal = new QTcpServer(this);
260 serverLocal->listen();
261 connect(serverLocal, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
262 serverRemote = new QTcpServer(this);
263 serverRemote->listen();
264 connect(serverRemote, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
266 QString rep1 = skype->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id).arg(serverLocal->serverPort()));
267 QString rep2 = skype->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id).arg(serverRemote->serverPort()));
269 if (!rep1.startsWith("ALTER CALL ") || !rep2.startsWith("ALTER CALL")) {
270 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
271 QString(PROGRAM_NAME " could not obtain the audio streams from Skype and can thus not record this call.\n\n"
272 "The replies from Skype were:\n%1\n%2").arg(rep1, rep2));
273 box->setWindowModality(Qt::NonModal);
274 box->setAttribute(Qt::WA_DeleteOnClose);
275 box->show();
276 removeFile();
277 delete writer;
278 delete serverRemote;
279 delete serverLocal;
280 return;
283 isRecording = true;
284 emit startedRecording();
287 void Call::acceptLocal() {
288 socketLocal = serverLocal->nextPendingConnection();
289 serverLocal->close();
290 // we don't delete the server, since it contains the socket.
291 // we could reparent, but that automatic stuff of QT is great
292 connect(socketLocal, SIGNAL(readyRead()), this, SLOT(readLocal()));
293 connect(socketLocal, SIGNAL(disconnected()), this, SLOT(checkConnections()));
296 void Call::acceptRemote() {
297 socketRemote = serverRemote->nextPendingConnection();
298 serverRemote->close();
299 connect(socketRemote, SIGNAL(readyRead()), this, SLOT(readRemote()));
300 connect(socketRemote, SIGNAL(disconnected()), this, SLOT(checkConnections()));
303 void Call::readLocal() {
304 bufferLocal += socketLocal->readAll();
305 if (isRecording)
306 tryToWrite();
309 void Call::readRemote() {
310 bufferRemote += socketRemote->readAll();
311 if (isRecording)
312 tryToWrite();
315 void Call::checkConnections() {
316 if (socketLocal->state() == QAbstractSocket::UnconnectedState && socketRemote->state() == QAbstractSocket::UnconnectedState) {
317 debug(QString("Call %1: both connections closed, stop recording").arg(id));
318 stopRecording();
322 void Call::mixToMono(int samples) {
323 long offset = bufferMono.size();
324 bufferMono.resize(offset + samples * 2);
326 qint16 *monoData = reinterpret_cast<qint16 *>(bufferMono.data()) + offset;
327 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
328 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
330 for (int i = 0; i < samples; i++) {
331 long sum = localData[i] + remoteData[i];
332 if (sum < -32768)
333 sum = -32768;
334 else if (sum > 32767)
335 sum = 32767;
336 monoData[i] = sum;
339 bufferLocal.remove(0, samples * 2);
340 bufferRemote.remove(0, samples * 2);
343 void Call::tryToWrite(bool flush) {
344 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
346 int l = bufferLocal.size();
347 int r = bufferRemote.size();
348 int samples = (l < r ? l : r) / 2;
350 // skype usually sends us more PCM data every 10ms, i.e. 160 samples.
351 // (skype operates at 16kHz) let's accumulate at least 100ms of data
352 // before bothering to write it to disk
353 if (samples < 1600 && !flush)
354 return;
356 // got new samples to write to file, or have to flush
358 bool success;
360 if (channelMode == 0) {
361 // mono
362 mixToMono(samples);
363 success = writer->write(bufferMono, bufferMono, samples, flush);
364 } else if (channelMode == 1) {
365 // stereo
366 success = writer->write(bufferLocal, bufferRemote, samples, flush);
367 } else if (channelMode == 2) {
368 // oerets
369 success = writer->write(bufferRemote, bufferLocal, samples, flush);
370 } else {
371 success = false;
374 if (!success) {
375 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
376 QString(PROGRAM_NAME " encountered an error while writing this call to disk. Recording terminated."));
377 box->setWindowModality(Qt::NonModal);
378 box->setAttribute(Qt::WA_DeleteOnClose);
379 box->show();
380 stopRecording(false);
381 return;
384 // the writer will remove the samples from the buffers
385 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
387 // TODO: handle the case where the two streams get out of sync (buffers
388 // not equally fulled by a significant amount). does skype document
389 // whether we always get two nice, equal, in-sync streams, even if
390 // there have been transmission errors? perl-script behavior: if out
391 // of sync by more than 6.4ms, then remove 1ms from the stream that's
392 // ahead.
395 void Call::stopRecording(bool flush) {
396 if (!isRecording)
397 return;
399 debug(QString("Call %1: stop recording").arg(id));
401 // NOTE: we don't delete the sockets here, because we may be here as a
402 // reaction to their disconnected() signals; and they don't like being
403 // deleted during their signals. we don't delete the servers either,
404 // since they own the sockets and we're too lazy to reparent. it's
405 // easiest to let QT handle all this on its own. there will be some
406 // memory wasted if you start/stop recording within the same call a few
407 // times, but unless you do it thousands of times, the waste is more
408 // than acceptable.
410 // flush data to writer
411 if (flush)
412 tryToWrite(true);
413 writer->close();
414 delete writer;
416 // we must disconnect all signals from the sockets first, so that upon
417 // closing them it won't call checkConnections() and we don't land here
418 // recursively again
419 disconnect(socketLocal, 0, this, 0);
420 disconnect(socketRemote, 0, this, 0);
421 socketLocal->close();
422 socketRemote->close();
424 isRecording = false;
425 emit stoppedRecording();
428 // ---- CallHandler ----
430 CallHandler::CallHandler(Skype *s) : skype(s), currentCall(-1) {
433 void CallHandler::callCmd(const QStringList &args) {
434 CallID id = args.at(0).toInt();
436 if (ignore.contains(id))
437 return;
439 bool newCall = false;
441 Call *call;
443 if (calls.contains(id)) {
444 call = calls[id];
445 } else {
446 call = new Call(this, skype, id);
447 calls[id] = call;
448 newCall = true;
450 connect(call, SIGNAL(startedCall(const QString &)), this, SIGNAL(startedCall(const QString &)));
451 connect(call, SIGNAL(stoppedCall()), this, SIGNAL(stoppedCall()));
452 connect(call, SIGNAL(startedRecording()), this, SIGNAL(startedRecording()));
453 connect(call, SIGNAL(stoppedRecording()), this, SIGNAL(stoppedRecording()));
456 // this holds the current call. skype currently only allows for one
457 // call at any time, so this should work ok
458 currentCall = id;
460 QString subCmd = args.at(1);
462 if (subCmd == "STATUS") {
463 QString a = args.at(2);
464 call->setStatus(a);
466 if (a == "INPROGRESS")
467 call->startRecording();
469 // don't stop recording when we get "FINISHED". just wait for
470 // the connections to close so that we really get all the data
471 } else if (subCmd == "DURATION") {
472 /* this is where we start recording calls that are already running, for
473 example if the user starts this program after the call has been placed */
474 call->setStatus("INPROGRESS");
475 if (newCall)
476 call->startRecording();
479 QList<Call *> list = calls.values();
480 for (int i = 0; i < list.size(); i++) {
481 Call *c = list.at(i);
482 QString status = c->getStatus();
483 if ((status == "FINISHED" || status == "CANCELLED") && c->okToDelete()) {
484 calls.remove(c->getID());
485 delete c;
490 void CallHandler::startRecording() {
491 if (!calls.contains(currentCall))
492 return;
494 calls[currentCall]->startRecording(true);
497 void CallHandler::stopRecording() {
498 if (!calls.contains(currentCall))
499 return;
501 Call *call = calls[currentCall];
502 call->stopRecording();
503 call->hideConfirmation(2);
506 void CallHandler::stopRecordingAndDelete() {
507 if (!calls.contains(currentCall))
508 return;
510 Call *call = calls[currentCall];
511 call->stopRecording();
512 call->removeFile();
513 call->hideConfirmation(0);