Simplify the structure of example directory

Only include code relevant to usage of kvzRTP and put everything
in the same directory
This commit is contained in:
Aaro Altonen 2020-04-06 12:02:57 +03:00
parent 90e8caff73
commit fbe35d8d92
22 changed files with 26 additions and 718 deletions

View File

@ -48,43 +48,7 @@ If you want SRTP/ZRTP support, you must compile [Crypto++](https://www.cryptopp.
## Examples
We provide several simple and thoroughly commented examples on how to use kvzRTP, please see:
[How to create a simple RTP sender](examples/simple/rtp/sending.cc)
[How to use fragmented input with kvzRTP \(HEVC slices\)](examples/simple/rtp/sending_fragmented.cc)
[How to create a simple RTP receiver (hooking)](examples/simple/rtp/receiving_hook.cc)
NOTE: The hook should **not** be used for media processing. It should be rather used as interface between application and library where the frame handout happens.
[How to create a simple RTP receiver (polling)](examples/simple/rtp/receiving_poll.cc)
[How to create an RTCP instance (polling)](examples/simple/rtcp/rtcp_poll.cc)
[How to create an RTCP instance (hoooking)](examples/simple/rtcp/rtcp_hook.cc)
## Configuration
By default, kvzRTP does not require any configuration but if the participants are sending high-quality video, some things must be configured
[How to configure RTP sender for high-quality video](examples/simple/rtp/send_hq.cc)
[How to configure RTP receiver for high-quality video](examples/simple/rtp/recv_hq.cc)
[How to configure SRTP with ZRTP](examples/simple/rtp/srtp_zrtp.cc)
[How to configure SRTP with user-managed keys](examples/simple/rtp/srtp_user.cc)
### Memory ownership/deallocation
If you have not enabled the system call dispatcher, you don't need to worry about these
[Method 1, unique_ptr](examples/simple/rtp/deallocation_1.cc)
[Method 2, copying](examples/simple/rtp/deallocation_2.cc)
[Method 3, deallocation hook](examples/simple/rtp/deallocation_3.cc)
Please see [examples](examples/) directory for different kvzRTP examples
## Defines

View File

@ -1,8 +1,28 @@
# kvzRTP example codes
This directory contains directories full and simple. Simple directory contains code snippets that demonstrate how the library should be used. These miminal examples also show how the library can be used, but don't themselves do anything.
We provide several simple and thoroughly commented examples on how to use kvzRTP.
Full directory on the other hand shows the complete flow: these snippts encode HEVC/Opus from raw video, send this encoded data using the library and receive and reconstruct the received frames.
These code snippets contain a lot of code unrelated to kvzRTP.
[How to create a simple RTP sender](sending.cc)
[How to use fragmented input with kvzRTP \(HEVC slices\)](sending_fragmented.cc)
[How to create a simple RTP receiver (hooking)](receiving_hook.cc)
NOTE: The hook should **not** be used for media processing. It should be rather used as interface between application and library where the frame handout happens.
[How to create a simple RTP receiver (polling)](receiving_poll.cc)
[How to create an RTCP instance (polling)](rtcp_poll.cc)
[How to create an RTCP instance (hoooking)](rtcp_hook.cc)
### Memory ownership/deallocation
If you have not enabled the system call dispatcher, you don't need to worry about these
[Method 1, unique_ptr](deallocation_1.cc)
[Method 2, copying](deallocation_2.cc)
[Method 3, deallocation hook](deallocation_3.cc)
You can also use [Kvazzup](https://github.com/ultravideo/Kvazzup) to see a real-world example.

View File

@ -4,6 +4,7 @@
int main(void)
{
/* See sending.cc for more details */
kvz_rtp::context rtp_ctx;
/* start session with remote at ip 10.21.25.2

View File

@ -1,58 +0,0 @@
# Sending
## HEVC sender
Extract 8-bit yuv420 raw video from input.mp4 and start ffplay
```
ffmpeg -i input.mp4 -f rawvideo -pix_fmt yuv420p video.raw
ffplay -protocol_whitelist "file,rtp,udp" ../sdp/hevc.sdp
```
Compile the RTP Library and hevc_sender.cc and start the sender
```
cd ../..
make all -j8
cd examples/sending
g++ -o main hevc_sender.cc -lrtp -L ../.. -lpthread -lkvazaar
./main
```
## Opus sender
Extract signed 16-bit little endian PCM from input.mp4 and start ffplay
```
ffmpeg -i input.mp4 -f s16le -acodec pcm_s16le -ac 2 -ar 48000 output.raw
ffplay -acodec libopus -protocol_whitelist "file,rtp,udp" ../sdp/opus.sdp
```
Compile the RTP Library and opus_sender.cc and start the sender
```
cd ../..
make all -j8
cd examples/sending
g++ -o main opus_sender.cc -lrtp -L ../.. -lpthread -lopus
./main
```
# Receiving
## HEVC sender/receiver
Extract 8-bit yuv420 raw video from input.mp4
```
ffmpeg -i input.mp4 -f rawvideo -pix_fmt yuv420p video.raw
```
Compile the RTP Library and recv_example_1.cc or recv_example_2.cc and start the sender
```
cd ../..
make all -j8
cd examples/receiving
g++ -o main recv_example_1.cc -lrtp -L ../.. -lpthread -lkvazaar
./main
```

View File

@ -1,153 +0,0 @@
#include "../../src/lib.hh"
#include "../../src/util.hh"
#include "../../src/rtp_opus.hh"
#include <iostream>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <kvazaar.h>
#include <opus/opus.h>
#include <stdint.h>
void recv_thread(kvz_rtp::context *ctx)
{
FILE *fp = fopen("out.hevc", "w");
if (!fp) {
fprintf(stderr, "failed to open file\n");
return;
}
kvz_rtp::frame::rtp_frame *frame = nullptr;
kvz_rtp::reader *reader = ctx->create_reader("127.0.0.1", 8888, RTP_FORMAT_HEVC);
(void)reader->start();
int i = 0;
uint8_t hevc_start_codes[4] = { 0x0, 0x0, 0x0, 0x1 };
while ((frame = reader->pull_frame()) != nullptr && i != 30) {
fwrite(hevc_start_codes, sizeof(hevc_start_codes), 1, fp);
fwrite(frame->payload, frame->payload_len, 1, fp);
kvz_rtp::frame::dealloc_frame(frame);
i++;
}
fclose(fp);
std::cerr << "\n\n\ndone!\n\n\n";
}
int main(int arg, char **argv)
{
kvz_rtp::context rtp_ctx;
std::thread *t1 = new std::thread(recv_thread, &rtp_ctx);
kvz_rtp::writer *writer = rtp_ctx.create_writer("127.0.0.1", 8888, RTP_FORMAT_HEVC);
(void)writer->start();
uint8_t *buffer = (uint8_t *)malloc(500);
FILE *inputFile = fopen("video.raw", "r");
size_t r = 0;
int width = 720;
int height = 720;
kvz_encoder* enc = NULL;
const kvz_api * const api = kvz_api_get(8);
kvz_config* config = api->config_alloc();
api->config_init(config);
api->config_parse(config, "preset", "ultrafast");
config->width = width;
config->height = height;
config->hash = kvz_hash::KVZ_HASH_NONE;
config->intra_period = 5;
config->vps_period = 1;
config->qp = 32;
config->framerate_num = 30;
config->framerate_denom = 1;
enc = api->encoder_open(config);
if (!enc) {
fprintf(stderr, "Failed to open encoder.\n");
return EXIT_FAILURE;
}
kvz_picture *img_in[16];
for (uint32_t i = 0; i < 16; ++i) {
img_in[i] = api->picture_alloc_csp(KVZ_CSP_420, width, height);
}
uint8_t inputCounter = 0;
uint8_t outputCounter = 0;
uint32_t frameIn = 0;
bool done = false;
uint8_t *outData = (uint8_t *)malloc(1024 * 1024);
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
while (!done) {
kvz_data_chunk* chunks_out = NULL;
kvz_picture *img_rec = NULL;
kvz_picture *img_src = NULL;
uint32_t len_out = 0;
kvz_frame_info info_out;
if (!fread(img_in[inputCounter]->y, width*height, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->u, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->v, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!api->encoder_encode(enc,
img_in[inputCounter],
&chunks_out, &len_out, &img_rec, &img_src, &info_out))
{
fprintf(stderr, "Failed to encode image.\n");
for (uint32_t i = 0; i < 16; i++) {
api->picture_free(img_in[i]);
}
return EXIT_FAILURE;
}
inputCounter = (inputCounter + 1) % 16;
if (chunks_out == NULL && img_in == NULL) {
/* We are done since there is no more input and output left. */
goto cleanup;
}
if (chunks_out != NULL) {
uint64_t written = 0;
uint32_t dataPos = 0;
/* Write data into the output file. */
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
written += chunk->len;
}
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
memcpy(&outData[dataPos], chunk->data, chunk->len);
dataPos += chunk->len;
}
outputCounter = (outputCounter + 1) % 16;
if (writer->push_frame(outData, written, RTP_FORMAT_HEVC) < 0) {
std::cerr << "RTP push failure" << std::endl;
}
}
}
cleanup:
t1->join();
return 0;
}

View File

@ -1,141 +0,0 @@
#include "../../src/lib.hh"
#include "../../src/util.hh"
#include "../../src/rtp_opus.hh"
#include <iostream>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <kvazaar.h>
#include <opus/opus.h>
#include <stdint.h>
void receive_hook(void *arg, kvz_rtp::frame::rtp_frame *frame)
{
if (!frame || !arg) {
fprintf(stderr, "invalid param\n");
return;
}
fwrite(frame->payload, frame->payload_len, 1, (FILE *)arg);
kvz_rtp::frame::dealloc_frame(frame);
}
int main(int arg, char **argv)
{
FILE *fp = fopen("out.hevc", "w");
kvz_rtp::context rtp_ctx;
kvz_rtp::reader *reader = rtp_ctx.create_reader("127.0.0.1", 8888, RTP_FORMAT_HEVC);
kvz_rtp::writer *writer = rtp_ctx.create_writer("127.0.0.1", 8888, RTP_FORMAT_HEVC);
reader->install_recv_hook(fp, receive_hook);
(void)writer->start();
(void)reader->start();
uint8_t *buffer = (uint8_t *)malloc(500);
FILE *inputFile = fopen("video.raw", "r");
size_t r = 0;
int width = 720;
int height = 720;
kvz_encoder* enc = NULL;
const kvz_api * const api = kvz_api_get(8);
kvz_config* config = api->config_alloc();
api->config_init(config);
api->config_parse(config, "preset", "ultrafast");
config->width = width;
config->height = height;
config->hash = kvz_hash::KVZ_HASH_NONE;
config->intra_period = 5;
config->vps_period = 1;
config->qp = 32;
config->framerate_num = 30;
config->framerate_denom = 1;
enc = api->encoder_open(config);
if (!enc) {
fprintf(stderr, "Failed to open encoder.\n");
return EXIT_FAILURE;
}
kvz_picture *img_in[16];
for (uint32_t i = 0; i < 16; ++i) {
img_in[i] = api->picture_alloc_csp(KVZ_CSP_420, width, height);
}
uint8_t inputCounter = 0;
uint8_t outputCounter = 0;
uint32_t frameIn = 0;
bool done = false;
uint8_t *outData = (uint8_t *)malloc(1024 * 1024);
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
while (!done) {
kvz_data_chunk* chunks_out = NULL;
kvz_picture *img_rec = NULL;
kvz_picture *img_src = NULL;
uint32_t len_out = 0;
kvz_frame_info info_out;
if (!fread(img_in[inputCounter]->y, width*height, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->u, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->v, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!api->encoder_encode(enc,
img_in[inputCounter],
&chunks_out, &len_out, &img_rec, &img_src, &info_out))
{
fprintf(stderr, "Failed to encode image.\n");
for (uint32_t i = 0; i < 16; i++) {
api->picture_free(img_in[i]);
}
return EXIT_FAILURE;
}
inputCounter = (inputCounter + 1) % 16;
if (chunks_out == NULL && img_in == NULL) {
/* We are done since there is no more input and output left. */
goto cleanup;
}
if (chunks_out != NULL) {
uint64_t written = 0;
uint32_t dataPos = 0;
/* Write data into the output file. */
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
written += chunk->len;
}
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
memcpy(&outData[dataPos], chunk->data, chunk->len);
dataPos += chunk->len;
}
outputCounter = (outputCounter + 1) % 16;
if (writer->push_frame(outData, written, RTP_FORMAT_HEVC) < 0) {
std::cerr << "RTP push failure" << std::endl;
}
}
}
cleanup:
return 0;
}

View File

@ -1,8 +0,0 @@
v=0
o=user 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 127.0.0.1
t=0 0
m=video 8888 RTP/AVP 96
a=rtpmap:96 H265/90000
a=recvonly

View File

@ -1,9 +0,0 @@
o=User 0 0 IN IP4 127.0.0.1
s=No Name
c=IN IP4 127.0.0.1
t=0 0
m=audio 8890 RTP/AVP 97
a=rtpmap:97 opus/48000/2
a=fmtp:97 stereo=1; useinbandfec=1; usedtx=0
a=ptime:20
a=maxptime:20

View File

@ -1,130 +0,0 @@
#include <kvzrtp/lib.hh>
#include <iostream>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <kvazaar.h>
#include <opus/opus.h>
#include <stdint.h>
#include <memory>
int main(void)
{
kvz_rtp::context rtp_ctx;
kvz_rtp::writer *writer = rtp_ctx.create_writer("127.0.0.1", 8888, RTP_FORMAT_HEVC);
(void)writer->start();
uint8_t *buffer = (uint8_t *)malloc(500);
FILE *inputFile = fopen("video.raw", "r");
size_t r = 0;
int width = 720;
int height = 720;
kvz_encoder* enc = NULL;
const kvz_api * const api = kvz_api_get(8);
kvz_config* config = api->config_alloc();
api->config_init(config);
api->config_parse(config, "preset", "ultrafast");
config->width = width;
config->height = height;
config->hash = kvz_hash::KVZ_HASH_NONE;
config->intra_period = 5;
config->vps_period = 1;
config->qp = 32;
config->framerate_num = 30;
config->framerate_denom = 1;
enc = api->encoder_open(config);
if (!enc) {
fprintf(stderr, "Failed to open encoder.\n");
return EXIT_FAILURE;
}
kvz_picture *img_in[16];
for (uint32_t i = 0; i < 16; ++i) {
img_in[i] = api->picture_alloc_csp(KVZ_CSP_420, width, height);
}
uint8_t inputCounter = 0;
uint8_t outputCounter = 0;
uint32_t frame = 0;
uint32_t frameIn = 0;
bool done = false;
int kzzz = 0;
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
while (!done) {
kvz_data_chunk* chunks_out = NULL;
kvz_picture *img_rec = NULL;
kvz_picture *img_src = NULL;
uint32_t len_out = 0;
kvz_frame_info info_out;
if (!fread(img_in[inputCounter]->y, width*height, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->u, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!fread(img_in[inputCounter]->v, width*height>>2, 1, inputFile)) {
done = true;
continue;
}
if (!api->encoder_encode(enc,
img_in[inputCounter],
&chunks_out, &len_out, &img_rec, &img_src, &info_out))
{
fprintf(stderr, "Failed to encode image.\n");
for (uint32_t i = 0; i < 16; i++) {
api->picture_free(img_in[i]);
}
return EXIT_FAILURE;
}
inputCounter = (inputCounter + 1) % 16;
if (chunks_out == NULL && img_in == NULL) {
// We are done since there is no more input and output left.
goto cleanup;
}
if (chunks_out != NULL) {
std::unique_ptr<uint8_t[]> outData = std::unique_ptr<uint8_t[]>(new uint8_t[1024 * 200]);
uint64_t written = 0;
uint32_t dataPos = 0;
// Write data into the output file.
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
written += chunk->len;
}
for (kvz_data_chunk *chunk = chunks_out; chunk != NULL; chunk = chunk->next) {
memcpy(&outData.get()[dataPos], chunk->data, chunk->len);
dataPos += chunk->len;
}
outputCounter = (outputCounter + 1) % 16;
frame++;
if (writer->push_frame(std::move(outData), written, RTP_FORMAT_HEVC) < 0) {
std::cerr << "RTP push failure" << std::endl;
}
/* if (frame > 31) */
/* goto cleanup; */
}
}
rtp_ctx.destroy_writer(writer);
cleanup:
return 0;
}

View File

@ -1,90 +0,0 @@
#include "../../src/debug.hh"
#include "../../src/lib.hh"
#include "../../src/util.hh"
#include "../../src/rtp_opus.hh"
#include <iostream>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <kvazaar.h>
#include <opus/opus.h>
#include <stdint.h>
#define OPUS_BITRATE 64000
// 20 ms for 48 000Hz
#define FRAME_SIZE 960
bool done = false;
void audioSender(RTPContext *ctx, int samplerate, int channels)
{
/* input has to PCM audio in signed 16-bit little endian format */
FILE *inFile = fopen("output.raw", "r");
int error = 0;
OpusEncoder* opusEnc = opus_encoder_create(samplerate, channels, OPUS_APPLICATION_VOIP, &error);
opus_encoder_ctl(opusEnc, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));
opus_encoder_ctl(opusEnc, OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_20_MS));
if (!opusEnc) {
std::cerr << "opus_encoder_create failed: " << error << std::endl;
return;
}
RTPOpus::OpusConfig *config = new RTPOpus::OpusConfig;
config->channels = channels;
config->samplerate = samplerate;
config->configurationNumber = 15; // Hydrib | FB | 20 ms
RTPWriter *writer = ctx->createWriter("127.0.0.1", 8890, RTP_FORMAT_OPUS);
writer->start();
writer->setConfig(config);
uint32_t dataLenPerFrame = FRAME_SIZE * channels * sizeof(uint16_t);
uint8_t *inFrame = (uint8_t *)malloc(dataLenPerFrame);
uint32_t outputSize = 25000;
uint8_t *outData = (uint8_t *)malloc(outputSize);
int frame = 0;
opus_int16 in[FRAME_SIZE * channels];
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
std::chrono::high_resolution_clock::time_point end;
while (!done) {
if (!fread(inFrame, dataLenPerFrame, 1, inFile)) {
done = true;
} else {
/* convert from little endian ordering */
for (int i = 0; i < channels * FRAME_SIZE; ++i) {
in[i] = inFrame[2 * i + 1] << 8 | inFrame[2 * i];
}
int32_t len = opus_encode(opusEnc, in, FRAME_SIZE, outData, outputSize);
// 20 ms per frame
if (writer->pushFrame(outData, len, RTP_FORMAT_OPUS, 960 * frame) < 0) {
std::cerr << "Failed to push Opus frame!" << std::endl;
}
frame++;
end = std::chrono::high_resolution_clock::now();
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
double diff = 20.0 - (elapsed_time / (double)frame);
std::this_thread::sleep_for(std::chrono::milliseconds((int)diff));
}
}
opus_encoder_destroy(opusEnc);
return;
}
int main(void)
{
RTPContext rtp_ctx;
std::thread *t = new std::thread(audioSender, &rtp_ctx, 48000, 2);
t->join();
std::cerr << "here" << std::endl;
}

View File

@ -1,8 +1,6 @@
#include <kvzrtp/lib.hh>
#include <thread>
#define USE_RECV_HOOK
void receive_hook(void *arg, kvz_rtp::frame::rtp_frame *frame)
{
if (!frame) {

View File

@ -1,6 +0,0 @@
# Simple examples
rtp directory contains examples showing how the RTP interface of the library should be used.
The examples are very simple and all they do is demonstrate how the library can be used.
rtcp directory contains RTCP examples

View File

@ -1,31 +0,0 @@
#include <kvzrtp/lib.hh>
void receive_hook(void *arg, kvz_rtp::frame::rtp_frame *frame)
{
(void)kvz_rtp::frame::dealloc_frame(frame);
}
int main(int argc, char **argv)
{
/* Enable system call dispatcher to improve sending speed */
kvz_rtp::context ctx;
kvz_rtp::reader *reader = ctx.create_reader("127.0.0.1", 5566, RTP_FORMAT_GENERIC);
/* Enable optimistic fragment receiver */
reader->configure(RCE_OPTIMISTIC_RECEIVER);
/* Increase the send UDP buffer size to 40 MB */
reader->configure(RCC_UDP_BUF_SIZE, 40 * 1024 * 1024);
/* Allocate space for 30 frames at the beginning of frame before they're
* spilled to temporary frames */
reader->configure(RCC_PROBATION_ZONE_SIZE, 30);
reader->install_recv_hook(NULL, receive_hook);
(void)reader->start();
(void)ctx.destroy_reader(reader);
return 0;
}

View File

@ -1,49 +0,0 @@
#include <kvzrtp/lib.hh>
#define PAYLOAD_MAXLEN 1 * 1000 * 1000
int main(int argc, char **argv)
{
kvz_rtp::context ctx;
kvz_rtp::writer *writer = ctx.create_writer("127.0.0.1", 5566, 8888, RTP_FORMAT_HEVC);
/* Enable system call dispatcher to improve sending speed */
writer->configure(RCE_SYSTEM_CALL_DISPATCHER);
/* Increase the send UDP buffer size to 40 MB */
writer->configure(RCC_UDP_BUF_SIZE, 40 * 1024 * 1024);
/* Cache 30 transactions to prevent constant (de)allocation */
writer->configure(RCC_MAX_TRANSACTIONS, 30);
/* Set max size for one input frame to 1.4 MB (1441 * 1000)
* (1.4 MB frame would equal 43 MB bitrate for a 30 FPS video which is very large) */
writer->configure(RCC_MAX_MESSAGES, 1000);
/* Before the writer can be used, it must be started.
* This initializes the underlying socket and all needed data structures */
(void)writer->start();
for (int i = 0; i < 10; ++i) {
/* We're using System Call Dispatcher so we must adhere to the memory ownership/deallocation
* rules defined in README.md
*
* Easiest way is to use smart pointers (as done here). If this memory was, however, received
* from f.ex. HEVC encoder directly and was not wrapped in a smart pointer, we could either
* install a deallocation hook for the memory or pass RTP_COPY to push_frame() to force kvzRTP
* to make a copy of the memory */
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[PAYLOAD_MAXLEN]);
/* We're using */
if (writer->push_frame(std::move(buffer), PAYLOAD_MAXLEN, RTP_NO_FLAGS) != RTP_OK) {
fprintf(stderr, "Failed to send RTP frame!");
}
}
/* Writer must be destroyed manually */
ctx.destroy_writer(writer);
return 0;
}