diff --git a/src/Makefile.am b/src/Makefile.am index 1c9461b5..cebc4084 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -23,6 +23,7 @@ mt_daapd_SOURCES = main.c daapd.h webserver.c \ httpd.c httpd.h \ httpd_rsp.c httpd_rsp.h \ httpd_daap.c httpd_daap.h \ + transcode.c transcode.h \ misc.c misc.h \ db-generic.c db-generic.h ff-plugins.c ff-plugins.h \ scan-wma.c \ diff --git a/src/transcode.c b/src/transcode.c new file mode 100644 index 00000000..e85a4cd3 --- /dev/null +++ b/src/transcode.c @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2009 Julien BLACHE + * + * Adapted from mt-daapd: + * Copyright (C) 2006-2007 Ron Pedde + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "daapd.h" +#include "err.h" +#include "conffile.h" +#include "ff-dbstruct.h" +#include "transcode.h" + + +#define XCODE_BUFFER_SIZE ((AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2) +#define RAW_BUFFER_SIZE 256 + +struct transcode_ctx { + AVFormatContext *fmtctx; + /* Audio stream */ + int astream; + AVCodecContext *acodec; /* pCodecCtx */ + AVCodec *adecoder; /* pCodec */ + AVPacket apacket; + int apacket_size; + uint8_t *apacket_data; + uint8_t *abuffer; + + off_t offset; + + uint32_t duration; + uint64_t samples; + + /* WAV header */ + uint8_t header[44]; + + /* Raw mode */ + int fd; + uint8_t *rawbuffer; +}; + + +static char *default_codecs = "mpeg,wav"; +static char *roku_codecs = "mpeg,mp4a,wma,wav"; +static char *itunes_codecs = "mpeg,mp4a,mp4v,alac,wav"; + + +static inline void +add_le16(uint8_t *dst, uint16_t val) +{ + dst[0] = val & 0xff; + dst[1] = (val >> 8) & 0xff; +} + +static inline void +add_le32(uint8_t *dst, uint32_t val) +{ + dst[0] = val & 0xff; + dst[1] = (val >> 8) & 0xff; + dst[2] = (val >> 16) & 0xff; + dst[3] = (val >> 24) & 0xff; +} + +static void +make_wav_header(struct transcode_ctx *ctx, size_t *est_size) +{ + uint32_t samplerate; + uint32_t byte_rate; + uint32_t wav_len; + int duration; + uint16_t channels; + uint16_t block_align; + uint16_t bits_per_sample; + + switch (ctx->acodec->sample_fmt) + { + case SAMPLE_FMT_S16: + bits_per_sample = 16; + break; + case SAMPLE_FMT_S32: + /* BROKEN */ + bits_per_sample = 32; + break; + default: + bits_per_sample = 16; + break; + } + + if (ctx->duration) + duration = ctx->duration; + else + duration = 3 * 60 * 1000; /* 3 minutes, in ms */ + + channels = ctx->acodec->channels; + samplerate = ctx->acodec->sample_rate; + + if (ctx->samples) + wav_len = ((bits_per_sample * channels / 8) * ctx->samples); + else + wav_len = ((bits_per_sample * samplerate * channels / 8) * (duration/1000)); + + *est_size = wav_len + sizeof(ctx->header); + + byte_rate = samplerate * channels * bits_per_sample / 8; + block_align = channels * bits_per_sample / 8; + + DPRINTF(E_DBG, L_XCODE, "WAV parameters: %d channels, %d kHz, %d bps\n", + channels, samplerate, bits_per_sample); + + memcpy(ctx->header, "RIFF", 4); + add_le32(ctx->header + 4, 36 + wav_len); + memcpy(ctx->header + 8, "WAVEfmt ", 8); + add_le32(ctx->header + 16, 16); + add_le16(ctx->header + 20, 1); + add_le16(ctx->header + 22, channels); + add_le32(ctx->header + 24, samplerate); + add_le32(ctx->header + 28, byte_rate); + add_le16(ctx->header + 32, block_align); + add_le16(ctx->header + 34, bits_per_sample); + memcpy(ctx->header + 36, "data", 4); + add_le32(ctx->header + 40, wav_len); +} + + +int +transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted) +{ + int processed; + int buflen; + int used; + int stop; + int ret; +#if BYTE_ORDER == BIG_ENDIAN + int i; + uint16_t *buf; +#endif + + processed = 0; + + if (ctx->offset == 0) + { + evbuffer_add(evbuf, ctx->header, sizeof(ctx->header)); + processed += sizeof(ctx->header); + ctx->offset += sizeof(ctx->header); + } + + stop = 0; + while ((processed < wanted) && !stop) + { + /* Decode data */ + while (ctx->apacket_size > 0) + { + buflen = XCODE_BUFFER_SIZE; + + used = avcodec_decode_audio2(ctx->acodec, + (int16_t *)ctx->abuffer, &buflen, + ctx->apacket_data, ctx->apacket_size); + + if (used < 0) + { + /* Something happened, skip this packet */ + ctx->apacket_size = 0; + break; + } + + ctx->apacket_data += used; + ctx->apacket_size -= used; + + /* No frame decoded this time around */ + if (buflen == 0) + continue; + +#if BYTE_ORDER == BIG_ENDIAN + /* swap buffer, le16 */ + buf = (uint16_t *)ctx->abuffer; + for (i = 0; i < (buflen / 2); i++) + { + buf[i] = htole16(buf[i]); + } +#endif + + ret = evbuffer_add(evbuf, ctx->abuffer, buflen); + if (ret != 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not copy WAV data to buffer\n"); + + return -1; + } + + processed += buflen; + } + + /* Read more data */ + if (ctx->fd != -1) + { + /* Raw mode */ + ret = read(ctx->fd, ctx->rawbuffer, RAW_BUFFER_SIZE); + if (ret <= 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not read more raw data\n"); + + stop = 1; + break; + } + + ctx->apacket_data = ctx->rawbuffer; + ctx->apacket_size = ret; + } + else + { + /* ffmpeg mode */ + do + { + if (ctx->apacket.data) + av_free_packet(&ctx->apacket); + + ret = av_read_packet(ctx->fmtctx, &ctx->apacket); + if (ret < 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not read more data\n"); + + stop = 1; + break; + } + } + while (ctx->apacket.stream_index != ctx->astream); + + /* Copy apacket data & size and do not mess with them */ + ctx->apacket_data = ctx->apacket.data; + ctx->apacket_size = ctx->apacket.size; + } + } + + ctx->offset += processed; + + return processed; +} + +struct transcode_ctx * +transcode_setup(struct media_file_info *mfi, size_t *est_size) +{ + struct transcode_ctx *ctx; + int i; + int ret; + + ctx = (struct transcode_ctx *)malloc(sizeof(struct transcode_ctx)); + if (!ctx) + { + DPRINTF(E_WARN, L_XCODE, "Could not allocate transcode context\n"); + + return NULL; + } + memset(ctx, 0, sizeof(struct transcode_ctx)); + ctx->fd = -1; + + ret = av_open_input_file(&ctx->fmtctx, mfi->path, NULL, 0, NULL); + if (ret != 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not open file %s: %s\n", mfi->fname, strerror(AVUNERROR(ret))); + + free(ctx); + return NULL; + } + + ret = av_find_stream_info(ctx->fmtctx); + if (ret < 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not find stream info: %s\n", strerror(AVUNERROR(ret))); + + goto setup_fail; + } + + ctx->astream = -1; + for (i = 0; i < ctx->fmtctx->nb_streams; i++) + { + if (ctx->fmtctx->streams[i]->codec->codec_type == CODEC_TYPE_AUDIO) + { + ctx->astream = i; + + break; + } + } + + if (ctx->astream < 0) + { + DPRINTF(E_WARN, L_XCODE, "No audio stream found in file %s\n", mfi->fname); + + goto setup_fail; + } + + ctx->acodec = ctx->fmtctx->streams[ctx->astream]->codec; + + ctx->adecoder = avcodec_find_decoder(ctx->acodec->codec_id); + if (!ctx->adecoder) + { + DPRINTF(E_WARN, L_XCODE, "No suitable decoder found for codec\n"); + + goto setup_fail; + } + + if (ctx->adecoder->capabilities & CODEC_CAP_TRUNCATED) + ctx->acodec->flags |= CODEC_FLAG_TRUNCATED; + + ret = avcodec_open(ctx->acodec, ctx->adecoder); + if (ret != 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not open codec: %s\n", strerror(AVUNERROR(ret))); + + goto setup_fail; + } + + /* FLAC needs raw mode, ffmpeg sucks */ + if (ctx->acodec->codec_id == CODEC_ID_FLAC) + { + ctx->rawbuffer = (uint8_t *)malloc(RAW_BUFFER_SIZE); + if (!ctx->rawbuffer) + { + DPRINTF(E_WARN, L_XCODE, "Could not allocate raw buffer\n"); + + avcodec_close(ctx->acodec); + goto setup_fail; + } + + ctx->fd = open(mfi->path, O_RDONLY); + if (ctx->fd < 0) + { + DPRINTF(E_WARN, L_XCODE, "Could not open %s: %s\n", mfi->fname, strerror(errno)); + + free(ctx->rawbuffer); + avcodec_close(ctx->acodec); + goto setup_fail; + } + } + + if (ctx->fd != -1) + DPRINTF(E_DBG, L_XCODE, "Set up raw mode for transcoding input\n"); + + ctx->abuffer = (uint8_t *)malloc(XCODE_BUFFER_SIZE); + if (!ctx->abuffer) + { + DPRINTF(E_WARN, L_XCODE, "Could not allocate transcode buffer\n"); + + if (ctx->fd != -1) + { + close(ctx->fd); + free(ctx->rawbuffer); + } + avcodec_close(ctx->acodec); + goto setup_fail; + } + + ctx->duration = mfi->song_length; + ctx->samples = mfi->sample_count; + + make_wav_header(ctx, est_size); + + return ctx; + + setup_fail: + av_close_input_file(ctx->fmtctx); + free(ctx); + return NULL; +} + +void +transcode_cleanup(struct transcode_ctx *ctx) +{ + if (ctx->fd != -1) + { + close(ctx->fd); + free(ctx->rawbuffer); + } + if (ctx->apacket.data) + av_free_packet(&ctx->apacket); + avcodec_close(ctx->acodec); + av_close_input_file(ctx->fmtctx); + + free(ctx); +} + + +int +transcode_needed(struct evkeyvalq *headers, char *file_codectype) +{ + const char *client_codecs; + const char *user_agent; + char *codectype; + cfg_t *lib; + int size; + int i; + + DPRINTF(E_DBG, L_XCODE, "Determining transcoding status for codectype %s\n", file_codectype); + + lib = cfg_getnsec(cfg, "library", 0); + + size = cfg_size(lib, "no_transcode"); + if (size > 0) + { + for (i = 0; i < size; i++) + { + codectype = cfg_getnstr(lib, "no_transcode", i); + + if (strcmp(file_codectype, codectype) == 0) + { + DPRINTF(E_DBG, L_XCODE, "Codectype is in no_transcode\n"); + + return 0; + } + } + } + + size = cfg_size(lib, "force_transcode"); + if (size > 0) + { + for (i = 0; i < size; i++) + { + codectype = cfg_getnstr(lib, "force_transcode", i); + + if (strcmp(file_codectype, codectype) == 0) + { + DPRINTF(E_DBG, L_XCODE, "Codectype is in force_transcode\n"); + + return 1; + } + } + } + + client_codecs = evhttp_find_header(headers, "Accept-Codecs"); + if (!client_codecs) + { + user_agent = evhttp_find_header(headers, "User-Agent"); + if (user_agent) + { + DPRINTF(E_DBG, L_XCODE, "User-Agent: %s\n", user_agent); + + if (strncmp(user_agent, "iTunes", strlen("iTunes")) == 0) + { + DPRINTF(E_DBG, L_XCODE, "Client is iTunes\n"); + + client_codecs = itunes_codecs; + } + else if (strncmp(user_agent, "Roku", strlen("Roku")) == 0) + { + DPRINTF(E_DBG, L_XCODE, "Client is a Roku device\n"); + + client_codecs = roku_codecs; + } + else if (strncmp(user_agent, "Hifidelio", strlen("Hifidelio")) == 0) + { + DPRINTF(E_DBG, L_XCODE, "Client is a Hifidelio device, allegedly cannot transcode\n"); + + /* Allegedly can't transcode for Hifidelio because their + * HTTP implementation doesn't honour Connection: close. + * At least, that's why mt-daapd didn't do it. + */ + return 0; + } + } + } + else + DPRINTF(E_DBG, L_XCODE, "Client advertises codecs: %s\n", client_codecs); + + if (!client_codecs) + { + DPRINTF(E_DBG, L_XCODE, "Could not identify client, using default codectype set\n"); + + client_codecs = default_codecs; + } + + if (strstr(client_codecs, file_codectype)) + { + DPRINTF(E_DBG, L_XCODE, "Codectype supported by client, no transcoding needed\n"); + return 0; + } + + DPRINTF(E_DBG, L_XCODE, "Will transcode\n"); + + return 1; +} diff --git a/src/transcode.h b/src/transcode.h new file mode 100644 index 00000000..b0a94c2c --- /dev/null +++ b/src/transcode.h @@ -0,0 +1,21 @@ + +#ifndef __TRANSCODE_H__ +#define __TRANSCODE_H__ + +#include + +struct transcode_ctx; + +int +transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted); + +struct transcode_ctx * +transcode_setup(struct media_file_info *mfi, size_t *est_size); + +void +transcode_cleanup(struct transcode_ctx *ctx); + +int +transcode_needed(struct evkeyvalq *headers, char *file_codectype); + +#endif /* !__TRANSCODE_H__ */