Implement new class RecordConfirmationDialog
[skype-call-recorder.git] / call.cpp
blobb390fb17c7c5d98c791f19570efc14e8eb2cbf97
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, const QString &sn, CallID i) :
49 skype(sk),
50 skypeName(sn),
51 id(i),
52 writer(NULL),
53 isRecording(false)
55 debug(QString("Call %1: Call object contructed").arg(id));
57 // Call objects track calls even before they are in progress and also
58 // when they are not being recorded.
60 // TODO check if we actually should record this call here
61 // and ask if we're unsure
64 Call::~Call() {
65 debug(QString("Call %1: Call object destructed").arg(id));
67 if (isRecording)
68 stopRecording();
70 // QT takes care of deleting servers and sockets
73 namespace {
74 QString escape(const QString &s) {
75 QString out = s;
76 out.replace('%', "%%");
77 out.replace('&', "&&");
78 return out;
82 QString Call::getFileName() const {
83 QString path = preferences.get("output.path").toString();
84 QString fileName = preferences.get("output.pattern").toString();
86 path.replace('~', QDir::homePath());
88 fileName.replace("&s", escape(skypeName));
89 // TODO
90 //fileName.replace("&f", escape(fullName));
91 //fileName.replace("&t", escape(mySkypeName));
92 //fileName.replace("&g", escape(myFullName));
93 fileName.replace("&&", "&");
95 // TODO: uhm, does QT provide any time formatting the strftime() way?
96 char *buf = new char[fileName.size() + 1024];
97 time_t t = std::time(NULL);
98 struct tm *tm = std::localtime(&t);
99 std::strftime(buf, fileName.size() + 1024, fileName.toUtf8().constData(), tm);
100 fileName = buf;
101 delete[] buf;
103 return path + '/' + fileName;
106 void Call::startRecording() {
107 if (isRecording)
108 return;
110 debug(QString("Call %1: start recording").arg(id));
112 // set up encoder for appropriate format
114 QString fileName = getFileName();
116 QString sm = preferences.get("output.channelmode").toString();
118 if (sm == "mono")
119 channelMode = 0;
120 else if (sm == "oerets")
121 channelMode = 2;
122 else /* if (sm == "stereo") */
123 channelMode = 1;
125 QString format = preferences.get("output.format").toString();
127 if (format == "wav")
128 writer = new WaveWriter;
129 else /* if (format == "mp3") */
130 writer = new Mp3Writer;
132 bool b = writer->open(fileName, 16000, channelMode != 0);
134 if (!b) {
135 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
136 QString(PROGRAM_NAME " could not open the file %1. Please verify the output file pattern.").arg(fileName));
137 box->setWindowModality(Qt::NonModal);
138 box->setAttribute(Qt::WA_DeleteOnClose);
139 box->show();
140 writer->remove();
141 delete writer;
142 return;
145 serverLocal = new QTcpServer(this);
146 serverLocal->listen();
147 connect(serverLocal, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
148 serverRemote = new QTcpServer(this);
149 serverRemote->listen();
150 connect(serverRemote, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
152 QString rep1 = skype->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id).arg(serverLocal->serverPort()));
153 QString rep2 = skype->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id).arg(serverRemote->serverPort()));
155 if (!rep1.startsWith("ALTER CALL ") || !rep2.startsWith("ALTER CALL")) {
156 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
157 QString(PROGRAM_NAME " could not obtain the audio streams from Skype and can thus not record this call.\n\n"
158 "The replies from Skype were:\n%1\n%2").arg(rep1).arg(rep2));
159 box->setWindowModality(Qt::NonModal);
160 box->setAttribute(Qt::WA_DeleteOnClose);
161 box->show();
162 writer->remove();
163 delete writer;
164 delete serverRemote;
165 delete serverLocal;
166 return;
169 isRecording = true;
172 void Call::acceptLocal() {
173 socketLocal = serverLocal->nextPendingConnection();
174 serverLocal->close();
175 // we don't delete the server, since it contains the socket.
176 // we could reparent, but that automatic stuff of QT is great
177 connect(socketLocal, SIGNAL(readyRead()), this, SLOT(readLocal()));
178 connect(socketLocal, SIGNAL(disconnected()), this, SLOT(checkConnections()));
181 void Call::acceptRemote() {
182 socketRemote = serverRemote->nextPendingConnection();
183 serverRemote->close();
184 connect(socketRemote, SIGNAL(readyRead()), this, SLOT(readRemote()));
185 connect(socketRemote, SIGNAL(disconnected()), this, SLOT(checkConnections()));
188 void Call::readLocal() {
189 bufferLocal += socketLocal->readAll();
190 tryToWrite();
193 void Call::readRemote() {
194 bufferRemote += socketRemote->readAll();
195 tryToWrite();
198 void Call::checkConnections() {
199 if (socketLocal->state() == QAbstractSocket::UnconnectedState && socketRemote->state() == QAbstractSocket::UnconnectedState) {
200 debug(QString("Call %1: both connections closed, stop recording").arg(id));
201 stopRecording();
205 void Call::mixToMono(int samples) {
206 long offset = bufferMono.size();
207 bufferMono.resize(offset + samples * 2);
209 qint16 *monoData = reinterpret_cast<qint16 *>(bufferMono.data()) + offset;
210 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
211 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
213 for (int i = 0; i < samples; i++) {
214 long sum = localData[i] + remoteData[i];
215 if (sum < -32768)
216 sum = -32768;
217 else if (sum > 32767)
218 sum = 32767;
219 monoData[i] = sum;
222 bufferLocal.remove(0, samples * 2);
223 bufferRemote.remove(0, samples * 2);
226 void Call::tryToWrite(bool flush) {
227 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
229 int l = bufferLocal.size();
230 int r = bufferRemote.size();
231 int samples = (l < r ? l : r) / 2;
233 if (!samples)
234 return;
236 // got new samples to write to file
238 bool success;
240 if (channelMode == 0) {
241 // mono
242 mixToMono(samples);
243 success = writer->write(bufferMono, bufferMono, samples, flush);
244 } else if (channelMode == 1) {
245 // stereo
246 success = writer->write(bufferLocal, bufferRemote, samples, flush);
247 } else if (channelMode == 2) {
248 // oerets
249 success = writer->write(bufferRemote, bufferLocal, samples, flush);
250 } else {
251 success = false;
254 if (!success) {
255 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
256 QString(PROGRAM_NAME " encountered an error while writing this call to disk. Recording terminated."));
257 box->setWindowModality(Qt::NonModal);
258 box->setAttribute(Qt::WA_DeleteOnClose);
259 box->show();
260 stopRecording(false);
261 return;
264 // the writer will remove the samples from the buffers
265 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
267 // TODO: handle the case where the two streams get out of sync (buffers
268 // not equally fulled by a significant amount). does skype document
269 // whether we always get two nice, equal, in-sync streams, even if
270 // there have been transmission errors? perl-script behavior: if out
271 // of sync by more than 6.4ms, then remove 1ms from the stream that's
272 // ahead.
275 void Call::stopRecording(bool flush) {
276 if (!isRecording)
277 return;
279 debug(QString("Call %1: stop recording").arg(id));
281 // NOTE: we don't delete the sockets here, because we may be here as a
282 // reaction to their disconnected() signals; and they don't like being
283 // deleted during their signals. we don't delete the servers either,
284 // since they own the sockets and we're too lazy to reparent. it's
285 // easiest to let QT handle all this on its own. there will be some
286 // memory wasted if you start/stop recording within the same call a few
287 // times, but unless you do it thousands of times, the waste is more
288 // than acceptable.
290 // flush data to writer
291 if (flush)
292 tryToWrite(true);
293 writer->close();
295 delete writer;
297 isRecording = false;
301 CallHandler::CallHandler(Skype *s) : skype(s) {
304 void CallHandler::closeAll() {
305 debug("closing all calls");
306 QList<Call *> list = calls.values();
307 for (int i = 0; i < list.size(); i++)
308 list.at(i)->stopRecording();
311 // ---- CallHandler ----
313 QString CallHandler::getObject(const QString &object) {
314 QString ret = skype->sendWithReply("GET " + object);
315 if (!ret.startsWith(object))
316 return QString();
317 return ret.mid(object.size() + 1);
320 void CallHandler::callCmd(const QStringList &args) {
321 CallID id = args.at(0).toInt();
323 if (ignore.contains(id))
324 return;
326 bool newCall = false;
328 if (!calls.contains(id)) {
329 QString skypeName = getObject(QString("CALL %1 PARTNER_HANDLE").arg(id));
330 if (skypeName.isEmpty()) {
331 debug(QString("Call %1: cannot get partner handle").arg(id));
332 ignore.insert(id);
333 return;
336 calls[id] = new Call(skype, skypeName, id);
337 newCall = true;
340 Call *call = calls[id];
342 QString subCmd = args.at(1);
344 if (subCmd == "STATUS") {
345 QString a = args.at(2);
346 if (a == "INPROGRESS")
347 call->startRecording();
348 else if (a == "DURATION") {
349 /* this is where we start recording calls that are already running, for
350 example if the user starts this program after the call has been placed */
351 if (newCall)
352 call->startRecording();
355 // don't stop recording when we get "FINISHED". just wait for
356 // the connections to close so that we really get all the data
360 // ---- RecordConfirmationDialog ----
362 RecordConfirmationDialog::RecordConfirmationDialog(const QString &skypeName, const QString &displayName) {
363 setWindowTitle(PROGRAM_NAME);
364 setAttribute(Qt::WA_DeleteOnClose);
366 QHBoxLayout *bighbox = new QHBoxLayout(this);
367 bighbox->setSizeConstraint(QLayout::SetFixedSize);
369 // get standard icon
370 int iconSize = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize);
371 QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
372 QLabel *iconLabel = new QLabel;
373 iconLabel->setPixmap(icon.pixmap(iconSize, iconSize));
374 bighbox->addWidget(iconLabel, 0, Qt::AlignTop);
376 bighbox->addSpacing(10);
378 QVBoxLayout *vbox = new QVBoxLayout;
379 bighbox->addLayout(vbox);
380 QLabel *label = new QLabel(QString("Do you wish to record this call with <b>%1</b> (%2)?").arg(skypeName).arg(displayName));
381 vbox->addWidget(label);
382 QCheckBox *check = new QCheckBox("Automatically perform this action on the next call with this person");
383 check->setEnabled(false);
384 vbox->addWidget(check);
385 QHBoxLayout *hbox = new QHBoxLayout;
386 QPushButton *button = new QPushButton("Yes, record this call");
387 connect(button, SIGNAL(clicked()), this, SLOT(yesClicked()));
388 hbox->addWidget(button);
389 button = new QPushButton("Do not record this call");
390 connect(button, SIGNAL(clicked()), this, SLOT(noClicked()));
391 hbox->addWidget(button);
392 vbox->addLayout(hbox);
394 connect(this, SIGNAL(rejected()), this, SIGNAL(no()));
396 show();
397 raise();
398 activateWindow();
401 void RecordConfirmationDialog::yesClicked() {
402 emit yes();
403 // TODO update preferences depending on checkbox
404 accept();
407 void RecordConfirmationDialog::noClicked() {
408 emit no();
409 // TODO update preferences depending on checkbox
410 accept();