Support for calls on hold
[skype-call-recorder.git] / call.cpp
blob0f3e3f5ddb8d6927e81c64d02283a557025c72c4
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>
30 #include <cstdlib>
32 #include "call.h"
33 #include "common.h"
34 #include "skype.h"
35 #include "wavewriter.h"
36 #include "mp3writer.h"
37 #include "preferences.h"
38 #include "callgui.h"
41 Call::Call(QObject *p, Skype *sk, CallID i) :
42 QObject(p),
43 skype(sk),
44 id(i),
45 status("UNKNOWN"),
46 writer(NULL),
47 isRecording(false),
48 shouldRecord(1)
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 bool Call::statusActive() const {
101 return status == "INPROGRESS" ||
102 status == "ONHOLD" ||
103 status == "LOCALHOLD" ||
104 status == "REMOTEHOLD";
107 void Call::setStatus(const QString &s) {
108 bool wasActive = statusActive();
109 status = s;
110 bool nowActive = statusActive();
112 if (!wasActive && nowActive)
113 emit startedCall(id, skypeName);
114 else if (wasActive && !nowActive)
115 emit stoppedCall(id);
118 bool Call::statusDone() const {
119 return status == "BUSY" ||
120 status == "CANCELLED" ||
121 status == "FAILED" ||
122 status == "FINISHED" ||
123 status == "MISSED" ||
124 status == "REFUSED";
125 // TODO: see what the deal is with REDIAL_PENDING (protocol 8)
128 QString Call::constructFileName() const {
129 return getFileName(skypeName, displayName, skype->getSkypeName(),
130 skype->getObject("PROFILE FULLNAME"), timeStartRecording);
133 QString Call::constructCommentTag() const {
134 QString str("Skype call between %1%2 and %3%4.");
135 QString dn1, dn2;
136 if (!displayName.isEmpty())
137 dn1 = QString(" (") + displayName + ")";
138 dn2 = skype->getObject("PROFILE FULLNAME");
139 if (!dn2.isEmpty())
140 dn2 = QString(" (") + dn2 + ")";
141 return str.arg(skypeName, dn1, skype->getSkypeName(), dn2);
144 void Call::setShouldRecord() {
145 // this sets shouldRecord based on preferences. shouldRecord is 0 if
146 // the call should not be recorded, 1 if we should ask and 2 if we
147 // should record
149 QStringList list = preferences.get("autorecord.yes").toList();
150 if (list.contains(skypeName)) {
151 shouldRecord = 2;
152 return;
155 list = preferences.get("autorecord.ask").toList();
156 if (list.contains(skypeName)) {
157 shouldRecord = 1;
158 return;
161 list = preferences.get("autorecord.no").toList();
162 if (list.contains(skypeName)) {
163 shouldRecord = 0;
164 return;
167 QString def = preferences.get("autorecord.default").toString();
168 if (def == "yes")
169 shouldRecord = 2;
170 else if (def == "ask")
171 shouldRecord = 1;
172 else if (def == "no")
173 shouldRecord = 0;
174 else
175 shouldRecord = 1;
178 void Call::ask() {
179 confirmation = new RecordConfirmationDialog(skypeName, displayName);
180 connect(confirmation, SIGNAL(yes()), this, SLOT(confirmRecording()));
181 connect(confirmation, SIGNAL(no()), this, SLOT(denyRecording()));
184 void Call::hideConfirmation(int should) {
185 if (confirmation) {
186 delete confirmation;
187 shouldRecord = should;
191 void Call::confirmRecording() {
192 shouldRecord = 2;
193 emit showLegalInformation();
196 void Call::denyRecording() {
197 // note that the call might already be finished by now
198 shouldRecord = 0;
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 emit showLegalInformation();
217 } else {
218 setShouldRecord();
219 if (shouldRecord == 0)
220 return;
221 if (shouldRecord == 1)
222 ask();
223 else // shouldRecord == 2
224 emit showLegalInformation();
227 debug(QString("Call %1: start recording").arg(id));
229 // set up encoder for appropriate format
231 timeStartRecording = QDateTime::currentDateTime();
232 QString fn = constructFileName();
234 QString sm = preferences.get("output.channelmode").toString();
236 if (sm == "mono")
237 channelMode = 0;
238 else if (sm == "oerets")
239 channelMode = 2;
240 else /* if (sm == "stereo") */
241 channelMode = 1;
243 QString format = preferences.get("output.format").toString();
245 if (format == "wav")
246 writer = new WaveWriter;
247 else /* if (format == "mp3") */
248 writer = new Mp3Writer;
250 if (preferences.get("output.savetags").toBool())
251 writer->setTags(constructCommentTag(), timeStartRecording);
253 bool b = writer->open(fn, 16000, channelMode != 0);
254 fileName = writer->fileName();
256 if (!b) {
257 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
258 QString(PROGRAM_NAME " could not open the file %1. Please verify the output file pattern.").arg(fileName));
259 box->setWindowModality(Qt::NonModal);
260 box->setAttribute(Qt::WA_DeleteOnClose);
261 box->show();
262 removeFile();
263 delete writer;
264 return;
267 serverLocal = new QTcpServer(this);
268 serverLocal->listen();
269 connect(serverLocal, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
270 serverRemote = new QTcpServer(this);
271 serverRemote->listen();
272 connect(serverRemote, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
274 QString rep1 = skype->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id).arg(serverLocal->serverPort()));
275 QString rep2 = skype->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id).arg(serverRemote->serverPort()));
277 if (!rep1.startsWith("ALTER CALL ") || !rep2.startsWith("ALTER CALL")) {
278 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
279 QString(PROGRAM_NAME " could not obtain the audio streams from Skype and can thus not record this call.\n\n"
280 "The replies from Skype were:\n%1\n%2").arg(rep1, rep2));
281 box->setWindowModality(Qt::NonModal);
282 box->setAttribute(Qt::WA_DeleteOnClose);
283 box->show();
284 removeFile();
285 delete writer;
286 delete serverRemote;
287 delete serverLocal;
288 return;
291 isRecording = true;
292 emit startedRecording(id);
295 void Call::acceptLocal() {
296 socketLocal = serverLocal->nextPendingConnection();
297 serverLocal->close();
298 // we don't delete the server, since it contains the socket.
299 // we could reparent, but that automatic stuff of QT is great
300 connect(socketLocal, SIGNAL(readyRead()), this, SLOT(readLocal()));
301 connect(socketLocal, SIGNAL(disconnected()), this, SLOT(checkConnections()));
304 void Call::acceptRemote() {
305 socketRemote = serverRemote->nextPendingConnection();
306 serverRemote->close();
307 connect(socketRemote, SIGNAL(readyRead()), this, SLOT(readRemote()));
308 connect(socketRemote, SIGNAL(disconnected()), this, SLOT(checkConnections()));
311 void Call::readLocal() {
312 bufferLocal += socketLocal->readAll();
313 if (isRecording)
314 tryToWrite();
317 void Call::readRemote() {
318 bufferRemote += socketRemote->readAll();
319 if (isRecording)
320 tryToWrite();
323 void Call::checkConnections() {
324 if (socketLocal->state() == QAbstractSocket::UnconnectedState && socketRemote->state() == QAbstractSocket::UnconnectedState) {
325 debug(QString("Call %1: both connections closed, stop recording").arg(id));
326 stopRecording();
330 void Call::mixToMono(long samples) {
331 long offset = bufferMono.size();
332 bufferMono.resize(offset + samples * 2);
334 qint16 *monoData = reinterpret_cast<qint16 *>(bufferMono.data()) + offset;
335 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
336 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
338 for (long i = 0; i < samples; i++) {
339 long sum = localData[i] + remoteData[i];
340 if (sum < -32768)
341 sum = -32768;
342 else if (sum > 32767)
343 sum = 32767;
344 monoData[i] = sum;
347 bufferLocal.remove(0, samples * 2);
348 bufferRemote.remove(0, samples * 2);
351 long Call::padBuffers() {
352 // pads the shorter buffer with silence, so they are both the same
353 // length afterwards. returns the new number of samples in each buffer
355 long l = bufferLocal.size();
356 long r = bufferRemote.size();
358 if (l < r) {
359 long amount = r - l;
360 bufferLocal.append(QByteArray(amount, 0));
361 debug(QString("Call %1: padding %2 samples on local buffer").arg(id).arg(amount / 2));
362 return r / 2;
363 } else if (l > r) {
364 long amount = l - r;
365 bufferRemote.append(QByteArray(amount, 0));
366 debug(QString("Call %1: padding %2 samples on remote buffer").arg(id).arg(amount / 2));
367 return l / 2;
370 return l / 2;
373 void Call::tryToWrite(bool flush) {
374 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
376 long samples; // number of samples to write
378 if (flush) {
379 // when flushing, we pad the shorter buffer, so that all
380 // available data is written. this shouldn't usually be a
381 // significant amount, but it might be if there was an audio
382 // I/O error in Skype.
383 samples = padBuffers();
384 } else {
385 long l = bufferLocal.size() / 2;
386 long r = bufferRemote.size() / 2;
388 if (std::labs(l - r) > 16000 * 10) {
389 // more than 10 seconds out of sync, something went
390 // wrong. avoid eating memory by accumulating data
391 debug(QString("Call %1: WARNING: seriously out of sync!").arg(id));
392 samples = padBuffers();
393 } else {
394 samples = l < r ? l : r;
396 // skype usually sends new PCM data every 10ms (160
397 // samples at 16kHz). let's accumulate at least 100ms
398 // of data before bothering to write it to disk
399 if (samples < 1600)
400 return;
404 // got new samples to write to file, or have to flush. note that we
405 // have to flush even if samples == 0
407 bool success;
409 if (channelMode == 0) {
410 // mono
411 mixToMono(samples);
412 QByteArray dummy;
413 success = writer->write(bufferMono, dummy, samples, flush);
414 } else if (channelMode == 1) {
415 // stereo
416 success = writer->write(bufferLocal, bufferRemote, samples, flush);
417 } else if (channelMode == 2) {
418 // oerets
419 success = writer->write(bufferRemote, bufferLocal, samples, flush);
420 } else {
421 success = false;
424 if (!success) {
425 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
426 QString(PROGRAM_NAME " encountered an error while writing this call to disk. Recording terminated."));
427 box->setWindowModality(Qt::NonModal);
428 box->setAttribute(Qt::WA_DeleteOnClose);
429 box->show();
430 stopRecording(false);
431 return;
434 // the writer will remove the samples from the buffers
435 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
437 // TODO: handle the case where the two streams get out of sync (buffers
438 // not equally fulled by a significant amount). does skype document
439 // whether we always get two nice, equal, in-sync streams, even if
440 // there have been transmission errors? perl-script behavior: if out
441 // of sync by more than 6.4ms, then remove 1ms from the stream that's
442 // ahead.
445 void Call::stopRecording(bool flush) {
446 if (!isRecording)
447 return;
449 debug(QString("Call %1: stop recording").arg(id));
451 // NOTE: we don't delete the sockets here, because we may be here as a
452 // reaction to their disconnected() signals; and they don't like being
453 // deleted during their signals. we don't delete the servers either,
454 // since they own the sockets and we're too lazy to reparent. it's
455 // easiest to let QT handle all this on its own. there will be some
456 // memory wasted if you start/stop recording within the same call a few
457 // times, but unless you do it thousands of times, the waste is more
458 // than acceptable.
460 // flush data to writer
461 if (flush)
462 tryToWrite(true);
463 writer->close();
464 delete writer;
466 // we must disconnect all signals from the sockets first, so that upon
467 // closing them it won't call checkConnections() and we don't land here
468 // recursively again
469 disconnect(socketLocal, 0, this, 0);
470 disconnect(socketRemote, 0, this, 0);
471 socketLocal->close();
472 socketRemote->close();
474 isRecording = false;
475 emit stoppedRecording(id);
478 // ---- CallHandler ----
480 CallHandler::CallHandler(QObject *parent, Skype *s) : QObject(parent), skype(s) {
483 CallHandler::~CallHandler() {
484 prune();
486 QList<Call *> list = calls.values();
487 if (!list.isEmpty()) {
488 debug(QString("Destroying CallHandler, these calls still exist:"));
489 for (int i = 0; i < list.size(); i++) {
490 Call *c = list.at(i);
491 debug(QString(" call %1, status=%2, okToDelete=%3").arg(c->getID()).arg(c->getStatus()).arg(c->okToDelete()));
495 delete legalInformationDialog;
498 void CallHandler::callCmd(const QStringList &args) {
499 CallID id = args.at(0).toInt();
501 if (ignore.contains(id))
502 return;
504 bool newCall = false;
506 Call *call;
508 if (calls.contains(id)) {
509 call = calls[id];
510 } else {
511 call = new Call(this, skype, id);
512 calls[id] = call;
513 newCall = true;
515 connect(call, SIGNAL(startedCall(int, const QString &)), this, SIGNAL(startedCall(int, const QString &)));
516 connect(call, SIGNAL(stoppedCall(int)), this, SIGNAL(stoppedCall(int)));
517 connect(call, SIGNAL(startedRecording(int)), this, SIGNAL(startedRecording(int)));
518 connect(call, SIGNAL(stoppedRecording(int)), this, SIGNAL(stoppedRecording(int)));
519 connect(call, SIGNAL(showLegalInformation()), this, SLOT(showLegalInformation()));
522 QString subCmd = args.at(1);
524 if (subCmd == "STATUS") {
525 QString a = args.at(2);
526 call->setStatus(a);
528 if (a == "INPROGRESS")
529 call->startRecording();
531 // don't stop recording when we get "FINISHED". just wait for
532 // the connections to close so that we really get all the data
533 } else if (subCmd == "DURATION") {
534 /* this is where we start recording calls that are already running, for
535 example if the user starts this program after the call has been placed */
536 call->setStatus("INPROGRESS");
537 if (newCall)
538 call->startRecording();
541 prune();
544 void CallHandler::prune() {
545 QList<Call *> list = calls.values();
546 for (int i = 0; i < list.size(); i++) {
547 Call *c = list.at(i);
548 if (c->statusDone() && c->okToDelete()) {
549 // we ignore this call from now on, because Skype might still send
550 // us information about it, like "SEEN" or "VAA_INPUT_STATUS"
551 calls.remove(c->getID());
552 ignore.insert(c->getID());
553 delete c;
558 void CallHandler::startRecording(int id) {
559 if (!calls.contains(id))
560 return;
562 calls[id]->startRecording(true);
565 void CallHandler::stopRecording(int id) {
566 if (!calls.contains(id))
567 return;
569 Call *call = calls[id];
570 call->stopRecording();
571 call->hideConfirmation(2);
574 void CallHandler::stopRecordingAndDelete(int id) {
575 if (!calls.contains(id))
576 return;
578 Call *call = calls[id];
579 call->stopRecording();
580 call->removeFile();
581 call->hideConfirmation(0);
584 void CallHandler::showLegalInformation() {
585 if (preferences.get("suppress.legalinformation").toBool())
586 return;
588 if (!legalInformationDialog)
589 legalInformationDialog = new LegalInformationDialog;
591 legalInformationDialog->raise();
592 legalInformationDialog->activateWindow();