/*
* Compile with (Linux only; in newly created directory toxcore/dir_name):
* gcc -o av_test ../toxav/av_test.c ../build/.libs/libtox*.a -lopencv_core \
* -lopencv_highgui -lopencv_imgproc -lsndfile -pthread -lvpx -lopus -lsodium -lportaudio
*/
/*
* Copyright © 2016-2017 The TokTok team.
* Copyright © 2013-2015 Tox project.
*
* This file is part of Tox, the free peer to peer instant messenger.
*
* Tox 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 3 of the License, or
* (at your option) any later version.
*
* Tox is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Tox. If not, see .
*/
#define _XOPEN_SOURCE 600
#ifdef __cplusplus
extern "C" {
#endif
// XXX: Hack because toxav doesn't really expose ring_buffer, but this av test
// uses it. Not all of these functions are used, but when linking statically,
// not renaming them will cause multiple definition errors, so we need to rename
// all of them.
#define RingBuffer TestRingBuffer
#define rb_full test_rb_full
#define rb_empty test_rb_empty
#define rb_write test_rb_write
#define rb_read test_rb_read
#define rb_new test_rb_new
#define rb_kill test_rb_kill
#define rb_size test_rb_size
#define rb_data test_rb_data
#include "../toxav/ring_buffer.c"
#include "../toxav/toxav.h"
#include "../toxcore/network.h" /* current_time_monotonic() */
#include "../toxcore/tox.h"
#include "../toxcore/util.h"
#ifdef __cplusplus
}
#endif
/* Playing audio data */
#include
/* Reading audio */
#include
/* Reading and Displaying video data */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define c_sleep(x) usleep(1000*(x))
#define CLIP(X) ((X) > 255 ? 255 : (X) < 0 ? 0 : X)
// RGB -> YUV
#define RGB2Y(R, G, B) CLIP((( 66 * (R) + 129 * (G) + 25 * (B) + 128) >> 8) + 16)
#define RGB2U(R, G, B) CLIP(((-38 * (R) - 74 * (G) + 112 * (B) + 128) >> 8) + 128)
#define RGB2V(R, G, B) CLIP(((112 * (R) - 94 * (G) - 18 * (B) + 128) >> 8) + 128)
// YUV -> RGB
#define C(Y) ((Y) - 16 )
#define D(U) ((U) - 128 )
#define E(V) ((V) - 128 )
#define YUV2R(Y, U, V) CLIP((298 * C(Y) + 409 * E(V) + 128) >> 8)
#define YUV2G(Y, U, V) CLIP((298 * C(Y) - 100 * D(U) - 208 * E(V) + 128) >> 8)
#define YUV2B(Y, U, V) CLIP((298 * C(Y) + 516 * D(U) + 128) >> 8)
#define TEST_TRANSFER_A 0
#define TEST_TRANSFER_V 1
typedef struct {
bool incoming;
uint32_t state;
pthread_mutex_t arb_mutex[1];
RingBuffer *arb; /* Audio ring buffer */
} CallControl;
struct toxav_thread_data {
ToxAV *AliceAV;
ToxAV *BobAV;
int32_t sig;
};
static const char *vdout = "AV Test"; /* Video output */
static PaStream *adout = nullptr; /* Audio output */
typedef struct {
uint16_t size;
int16_t data[];
} frame;
static void *pa_write_thread(void *d)
{
/* The purpose of this thread is to make sure Pa_WriteStream will not block
* toxav_iterate thread
*/
CallControl *cc = (CallControl *)d;
while (Pa_IsStreamActive(adout)) {
frame *f;
pthread_mutex_lock(cc->arb_mutex);
if (rb_read(cc->arb, (void **)&f)) {
pthread_mutex_unlock(cc->arb_mutex);
Pa_WriteStream(adout, f->data, f->size);
free(f);
} else {
pthread_mutex_unlock(cc->arb_mutex);
c_sleep(10);
}
}
return nullptr;
}
/**
* Callbacks
*/
static void t_toxav_call_cb(ToxAV *av, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data)
{
printf("Handling CALL callback\n");
((CallControl *)user_data)->incoming = true;
}
static void t_toxav_call_state_cb(ToxAV *av, uint32_t friend_number, uint32_t state, void *user_data)
{
printf("Handling CALL STATE callback: %d\n", state);
((CallControl *)user_data)->state = state;
}
static void t_toxav_receive_video_frame_cb(ToxAV *av, uint32_t friend_number,
uint16_t width, uint16_t height,
uint8_t const *y, uint8_t const *u, uint8_t const *v,
int32_t ystride, int32_t ustride, int32_t vstride,
void *user_data)
{
ystride = abs(ystride);
ustride = abs(ustride);
vstride = abs(vstride);
uint16_t *img_data = (uint16_t *)malloc(height * width * 6);
unsigned long int i, j;
for (i = 0; i < height; ++i) {
for (j = 0; j < width; ++j) {
uint8_t *point = (uint8_t *) img_data + 3 * ((i * width) + j);
int yx = y[(i * ystride) + j];
int ux = u[((i / 2) * ustride) + (j / 2)];
int vx = v[((i / 2) * vstride) + (j / 2)];
point[0] = YUV2R(yx, ux, vx);
point[1] = YUV2G(yx, ux, vx);
point[2] = YUV2B(yx, ux, vx);
}
}
CvMat mat = cvMat(height, width, CV_8UC3, img_data);
CvSize sz;
sz.height = height;
sz.width = width;
IplImage *header = cvCreateImageHeader(sz, 1, 3);
IplImage *img = cvGetImage(&mat, header);
cvShowImage(vdout, img);
free(img_data);
}
static void t_toxav_receive_audio_frame_cb(ToxAV *av, uint32_t friend_number,
int16_t const *pcm,
size_t sample_count,
uint8_t channels,
uint32_t sampling_rate,
void *user_data)
{
CallControl *cc = (CallControl *)user_data;
frame *f = (frame *)malloc(sizeof(uint16_t) + sample_count * sizeof(int16_t) * channels);
memcpy(f->data, pcm, sample_count * sizeof(int16_t) * channels);
f->size = sample_count;
pthread_mutex_lock(cc->arb_mutex);
free(rb_write(cc->arb, f));
pthread_mutex_unlock(cc->arb_mutex);
}
static void t_toxav_audio_bit_rate_cb(ToxAV *av, uint32_t friend_number,
uint32_t audio_bit_rate, void *user_data)
{
printf("Suggested bit rate: audio: %d\n", audio_bit_rate);
}
static void t_toxav_video_bit_rate_cb(ToxAV *av, uint32_t friend_number,
uint32_t video_bit_rate, void *user_data)
{
printf("Suggested bit rate: video: %d\n", video_bit_rate);
}
static void t_accept_friend_request_cb(Tox *m, const uint8_t *public_key, const uint8_t *data, size_t length,
void *userdata)
{
if (length == 7 && memcmp("gentoo", data, 7) == 0) {
assert(tox_friend_add_norequest(m, public_key, nullptr) != (uint32_t) ~0);
}
}
/**
*/
static void initialize_tox(Tox **bootstrap, ToxAV **AliceAV, CallControl *AliceCC, ToxAV **BobAV, CallControl *BobCC)
{
Tox *Alice;
Tox *Bob;
struct Tox_Options *opts = tox_options_new(nullptr);
assert(opts != nullptr);
tox_options_set_end_port(opts, 0);
tox_options_set_ipv6_enabled(opts, false);
{
TOX_ERR_NEW error;
tox_options_set_start_port(opts, 33445);
*bootstrap = tox_new(opts, &error);
assert(error == TOX_ERR_NEW_OK);
tox_options_set_start_port(opts, 33455);
Alice = tox_new(opts, &error);
assert(error == TOX_ERR_NEW_OK);
tox_options_set_start_port(opts, 33465);
Bob = tox_new(opts, &error);
assert(error == TOX_ERR_NEW_OK);
}
tox_options_free(opts);
printf("Created 3 instances of Tox\n");
printf("Preparing network...\n");
long long unsigned int cur_time = time(nullptr);
uint32_t to_compare = 974536;
uint8_t address[TOX_ADDRESS_SIZE];
tox_callback_friend_request(Alice, t_accept_friend_request_cb);
tox_self_get_address(Alice, address);
assert(tox_friend_add(Bob, address, (const uint8_t *)"gentoo", 7, nullptr) != (uint32_t) ~0);
uint8_t off = 1;
while (1) {
tox_iterate(*bootstrap, &to_compare);
tox_iterate(Alice, &to_compare);
tox_iterate(Bob, &to_compare);
if (tox_self_get_connection_status(*bootstrap) &&
tox_self_get_connection_status(Alice) &&
tox_self_get_connection_status(Bob) && off) {
printf("Toxes are online, took %llu seconds\n", time(nullptr) - cur_time);
off = 0;
}
if (tox_friend_get_connection_status(Alice, 0, nullptr) == TOX_CONNECTION_UDP &&
tox_friend_get_connection_status(Bob, 0, nullptr) == TOX_CONNECTION_UDP) {
break;
}
c_sleep(20);
}
TOXAV_ERR_NEW rc;
*AliceAV = toxav_new(Alice, &rc);
assert(rc == TOXAV_ERR_NEW_OK);
*BobAV = toxav_new(Bob, &rc);
assert(rc == TOXAV_ERR_NEW_OK);
/* Alice */
toxav_callback_call(*AliceAV, t_toxav_call_cb, AliceCC);
toxav_callback_call_state(*AliceAV, t_toxav_call_state_cb, AliceCC);
toxav_callback_audio_bit_rate(*AliceAV, t_toxav_audio_bit_rate_cb, AliceCC);
toxav_callback_video_bit_rate(*AliceAV, t_toxav_video_bit_rate_cb, AliceCC);
toxav_callback_video_receive_frame(*AliceAV, t_toxav_receive_video_frame_cb, AliceCC);
toxav_callback_audio_receive_frame(*AliceAV, t_toxav_receive_audio_frame_cb, AliceCC);
/* Bob */
toxav_callback_call(*BobAV, t_toxav_call_cb, BobCC);
toxav_callback_call_state(*BobAV, t_toxav_call_state_cb, BobCC);
toxav_callback_audio_bit_rate(*BobAV, t_toxav_audio_bit_rate_cb, BobCC);
toxav_callback_video_bit_rate(*BobAV, t_toxav_video_bit_rate_cb, BobCC);
toxav_callback_video_receive_frame(*BobAV, t_toxav_receive_video_frame_cb, BobCC);
toxav_callback_audio_receive_frame(*BobAV, t_toxav_receive_audio_frame_cb, BobCC);
printf("Created 2 instances of ToxAV\n");
printf("All set after %llu seconds!\n", time(nullptr) - cur_time);
}
static int iterate_tox(Tox *bootstrap, ToxAV *AliceAV, ToxAV *BobAV, void *userdata)
{
tox_iterate(bootstrap, userdata);
tox_iterate(toxav_get_tox(AliceAV), userdata);
tox_iterate(toxav_get_tox(BobAV), userdata);
return MIN(tox_iteration_interval(toxav_get_tox(AliceAV)), tox_iteration_interval(toxav_get_tox(BobAV)));
}
static void *iterate_toxav(void *data)
{
struct toxav_thread_data *data_cast = (struct toxav_thread_data *)data;
#if defined TEST_TRANSFER_V && TEST_TRANSFER_V == 1
cvNamedWindow(vdout, CV_WINDOW_AUTOSIZE);
#endif
while (data_cast->sig == 0) {
toxav_iterate(data_cast->AliceAV);
toxav_iterate(data_cast->BobAV);
int rc = MIN(toxav_iteration_interval(data_cast->AliceAV), toxav_iteration_interval(data_cast->BobAV));
printf("\rIteration interval: %d ", rc);
fflush(stdout);
#if defined TEST_TRANSFER_V && TEST_TRANSFER_V == 1
if (!rc) {
rc = 1;
}
cvWaitKey(rc);
#else
c_sleep(rc);
#endif
}
data_cast->sig = 1;
#if defined TEST_TRANSFER_V && TEST_TRANSFER_V == 1
cvDestroyWindow(vdout);
#endif
pthread_exit(nullptr);
}
static int send_opencv_img(ToxAV *av, uint32_t friend_number, const IplImage *img)
{
int32_t strides[3] = { img->width, img->width / 2, img->width / 2 };
uint8_t *planes[3] = {
(uint8_t *)malloc(img->height * img->width),
(uint8_t *)malloc(img->height * img->width / 4),
(uint8_t *)malloc(img->height * img->width / 4),
};
int x_chroma_shift = 1;
int y_chroma_shift = 1;
int x, y;
for (y = 0; y < img->height; ++y) {
for (x = 0; x < img->width; ++x) {
uint8_t r = img->imageData[(x + y * img->width) * 3 + 0];
uint8_t g = img->imageData[(x + y * img->width) * 3 + 1];
uint8_t b = img->imageData[(x + y * img->width) * 3 + 2];
planes[0][x + y * strides[0]] = RGB2Y(r, g, b);
if (!(x % (1 << x_chroma_shift)) && !(y % (1 << y_chroma_shift))) {
const int i = x / (1 << x_chroma_shift);
const int j = y / (1 << y_chroma_shift);
planes[1][i + j * strides[1]] = RGB2U(r, g, b);
planes[2][i + j * strides[2]] = RGB2V(r, g, b);
}
}
}
int rc = toxav_video_send_frame(av, friend_number, img->width, img->height,
planes[0], planes[1], planes[2], nullptr);
free(planes[0]);
free(planes[1]);
free(planes[2]);
return rc;
}
static int print_audio_devices(void)
{
int i = 0;
for (i = 0; i < Pa_GetDeviceCount(); ++i) {
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
if (info) {
printf("%d) %s\n", i, info->name);
}
}
return 0;
}
static int print_help(const char *name)
{
printf("Usage: %s -[a:v:o:dh]\n"
"-a audio input file\n"
"-b audio frame duration\n"
"-v video input file\n"
"-x video frame duration\n"
"-o output audio device index\n"
"-d print output audio devices\n"
"-h print this help\n", name);
return 0;
}
int main(int argc, char **argv)
{
freopen("/dev/zero", "w", stderr);
Pa_Initialize();
struct stat st;
/* AV files for testing */
const char *af_name = nullptr;
const char *vf_name = nullptr;
long audio_out_dev_idx = -1;
int32_t audio_frame_duration = 20;
#if 0
// TODO(mannol): Put this to use.
int32_t video_frame_duration = 10;
#endif
/* Parse settings */
CHECK_ARG:
switch (getopt(argc, argv, "a:b:v:x:o:dh")) {
case 'a':
af_name = optarg;
goto CHECK_ARG;
case 'b': {
char *d;
audio_frame_duration = strtol(optarg, &d, 10);
if (*d) {
printf("Invalid value for argument: 'b'");
exit(1);
}
goto CHECK_ARG;
}
case 'v':
vf_name = optarg;
goto CHECK_ARG;
#if 0
case 'x': {
char *d;
video_frame_duration = strtol(optarg, &d, 10);
if (*d) {
printf("Invalid value for argument: 'x'");
exit(1);
}
goto CHECK_ARG;
}
#endif
case 'o': {
char *d;
audio_out_dev_idx = strtol(optarg, &d, 10);
if (*d) {
printf("Invalid value for argument: 'o'");
exit(1);
}
goto CHECK_ARG;
}
case 'd':
return print_audio_devices();
case 'h':
return print_help(argv[0]);
case '?':
exit(1);
case -1:
;
}
{ /* Check files */
if (!af_name) {
printf("Required audio input file!\n");
exit(1);
}
if (!vf_name) {
printf("Required video input file!\n");
exit(1);
}
/* Check for files */
if (stat(af_name, &st) != 0 || !S_ISREG(st.st_mode)) {
printf("%s doesn't seem to be a regular file!\n", af_name);
exit(1);
}
if (stat(vf_name, &st) != 0 || !S_ISREG(st.st_mode)) {
printf("%s doesn't seem to be a regular file!\n", vf_name);
exit(1);
}
}
if (audio_out_dev_idx < 0) {
audio_out_dev_idx = Pa_GetDefaultOutputDevice();
}
const PaDeviceInfo *audio_dev = Pa_GetDeviceInfo(audio_out_dev_idx);
if (!audio_dev) {
fprintf(stderr, "Device under index: %ld invalid", audio_out_dev_idx);
return 1;
}
printf("Using audio device: %s\n", audio_dev->name);
printf("Using audio file: %s\n", af_name);
printf("Using video file: %s\n", vf_name);
/* START TOX NETWORK */
Tox *bootstrap;
ToxAV *AliceAV;
ToxAV *BobAV;
CallControl AliceCC;
CallControl BobCC;
initialize_tox(&bootstrap, &AliceAV, &AliceCC, &BobAV, &BobCC);
if (TEST_TRANSFER_A) {
SNDFILE *af_handle;
SF_INFO af_info;
printf("\nTrying audio enc/dec...\n");
memset(&AliceCC, 0, sizeof(CallControl));
memset(&BobCC, 0, sizeof(CallControl));
pthread_mutex_init(AliceCC.arb_mutex, nullptr);
pthread_mutex_init(BobCC.arb_mutex, nullptr);
AliceCC.arb = rb_new(16);
BobCC.arb = rb_new(16);
{ /* Call */
TOXAV_ERR_CALL rc;
toxav_call(AliceAV, 0, 48, 0, &rc);
if (rc != TOXAV_ERR_CALL_OK) {
printf("toxav_call failed: %d\n", rc);
exit(1);
}
}
while (!BobCC.incoming) {
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
}
{ /* Answer */
TOXAV_ERR_ANSWER rc;
toxav_answer(BobAV, 0, 48, 0, &rc);
if (rc != TOXAV_ERR_ANSWER_OK) {
printf("toxav_answer failed: %d\n", rc);
exit(1);
}
}
while (AliceCC.state == 0) {
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
}
/* Open audio file */
af_handle = sf_open(af_name, SFM_READ, &af_info);
if (af_handle == nullptr) {
printf("Failed to open the file.\n");
exit(1);
}
int16_t PCM[5760];
time_t start_time = time(nullptr);
time_t expected_time = af_info.frames / af_info.samplerate + 2;
/* Start decode thread */
struct toxav_thread_data data = {
AliceAV,
BobAV,
0,
};
pthread_t dect;
pthread_create(&dect, nullptr, iterate_toxav, &data);
pthread_detach(dect);
int frame_size = (af_info.samplerate * audio_frame_duration / 1000) * af_info.channels;
struct PaStreamParameters output;
output.device = audio_out_dev_idx;
output.channelCount = af_info.channels;
output.sampleFormat = paInt16;
output.suggestedLatency = audio_dev->defaultHighOutputLatency;
output.hostApiSpecificStreamInfo = nullptr;
PaError err = Pa_OpenStream(&adout, nullptr, &output, af_info.samplerate, frame_size, paNoFlag, nullptr, nullptr);
assert(err == paNoError);
err = Pa_StartStream(adout);
assert(err == paNoError);
// toxav_audio_bit_rate_set(AliceAV, 0, 64, false, nullptr);
/* Start write thread */
pthread_t t;
pthread_create(&t, nullptr, pa_write_thread, &BobCC);
pthread_detach(t);
printf("Sample rate %d\n", af_info.samplerate);
while (start_time + expected_time > time(nullptr)) {
uint64_t enc_start_time = current_time_monotonic();
int64_t count = sf_read_short(af_handle, PCM, frame_size);
if (count > 0) {
TOXAV_ERR_SEND_FRAME rc;
if (toxav_audio_send_frame(AliceAV, 0, PCM, count / af_info.channels, af_info.channels, af_info.samplerate,
&rc) == false) {
printf("Error sending frame of size %ld: %d\n", (long)count, rc);
}
}
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
c_sleep((audio_frame_duration - (current_time_monotonic() - enc_start_time) - 1));
}
printf("Played file in: %lu; stopping stream...\n", time(nullptr) - start_time);
Pa_StopStream(adout);
sf_close(af_handle);
{ /* Hangup */
TOXAV_ERR_CALL_CONTROL rc;
toxav_call_control(AliceAV, 0, TOXAV_CALL_CONTROL_CANCEL, &rc);
if (rc != TOXAV_ERR_CALL_CONTROL_OK) {
printf("toxav_call_control failed: %d\n", rc);
exit(1);
}
}
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
assert(BobCC.state == TOXAV_FRIEND_CALL_STATE_FINISHED);
/* Stop decode thread */
data.sig = -1;
while (data.sig != 1) {
sched_yield();
}
pthread_mutex_destroy(AliceCC.arb_mutex);
pthread_mutex_destroy(BobCC.arb_mutex);
void *f = nullptr;
while (rb_read(AliceCC.arb, &f)) {
free(f);
}
while (rb_read(BobCC.arb, &f)) {
free(f);
}
printf("Success!");
}
if (TEST_TRANSFER_V) {
printf("\nTrying video enc/dec...\n");
memset(&AliceCC, 0, sizeof(CallControl));
memset(&BobCC, 0, sizeof(CallControl));
{ /* Call */
TOXAV_ERR_CALL rc;
toxav_call(AliceAV, 0, 0, 2000, &rc);
if (rc != TOXAV_ERR_CALL_OK) {
printf("toxav_call failed: %d\n", rc);
exit(1);
}
}
while (!BobCC.incoming) {
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
}
{ /* Answer */
TOXAV_ERR_ANSWER rc;
toxav_answer(BobAV, 0, 0, 5000, &rc);
if (rc != TOXAV_ERR_ANSWER_OK) {
printf("toxav_answer failed: %d\n", rc);
exit(1);
}
}
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
/* Start decode thread */
struct toxav_thread_data data = {
AliceAV,
BobAV,
0,
};
pthread_t dect;
pthread_create(&dect, nullptr, iterate_toxav, &data);
pthread_detach(dect);
CvCapture *capture = cvCreateFileCapture(vf_name);
if (!capture) {
printf("Failed to open video file: %s\n", vf_name);
exit(1);
}
#if 0
toxav_video_bit_rate_set(AliceAV, 0, 5000, false, nullptr);
#endif
time_t start_time = time(nullptr);
while (start_time + 90 > time(nullptr)) {
IplImage *frame = cvQueryFrame(capture);
if (!frame) {
break;
}
send_opencv_img(AliceAV, 0, frame);
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
c_sleep(10);
}
cvReleaseCapture(&capture);
{ /* Hangup */
TOXAV_ERR_CALL_CONTROL rc;
toxav_call_control(AliceAV, 0, TOXAV_CALL_CONTROL_CANCEL, &rc);
if (rc != TOXAV_ERR_CALL_CONTROL_OK) {
printf("toxav_call_control failed: %d\n", rc);
exit(1);
}
}
iterate_tox(bootstrap, AliceAV, BobAV, nullptr);
assert(BobCC.state == TOXAV_FRIEND_CALL_STATE_FINISHED);
/* Stop decode thread */
printf("Stopping decode thread\n");
data.sig = -1;
while (data.sig != 1) {
sched_yield();
}
printf("Success!");
}
Tox *Alice = toxav_get_tox(AliceAV);
Tox *Bob = toxav_get_tox(BobAV);
toxav_kill(BobAV);
toxav_kill(AliceAV);
tox_kill(Bob);
tox_kill(Alice);
tox_kill(bootstrap);
printf("\nTest successful!\n");
Pa_Terminate();
return 0;
}