[xcode/daap/rsp] Default transcode to 320 kbps mp3 instead of wav

- Calculate size for both formats (+ move the return to transcode_encode_query)
- Let transcode_needed() decide what format to output
- Determine content-type from transcoding type
- Add transcode-dependent ability to override file metadata in rsp/daap
- Send file size matching format
This commit is contained in:
ejurgensen 2023-10-21 00:17:20 +02:00
parent 9394d45de1
commit 3ee9204ff8
17 changed files with 359 additions and 276 deletions

View File

@ -703,13 +703,13 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is
}
if (dst_format == ART_FMT_JPEG)
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, dst_width, dst_height);
else if (dst_format == ART_FMT_PNG)
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, dst_width, dst_height);
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, dst_width, dst_height);
else if (dst_format == ART_FMT_VP8)
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, NULL, dst_width, dst_height);
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, dst_width, dst_height);
else
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, dst_width, dst_height);
if (!xcode_encode)
{

View File

@ -360,12 +360,11 @@ dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errms
}
int
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav)
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags)
{
const struct dmap_field_map *dfm;
const struct dmap_field *df;
char **strval;
char *ptr;
int32_t val;
int want_mikd;
int want_asdk;
@ -444,40 +443,7 @@ dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, stru
continue;
}
val = 0;
if (force_wav)
{
switch (dfm->mfi_offset)
{
case dbmfi_offsetof(type):
ptr = "wav";
strval = &ptr;
break;
case dbmfi_offsetof(bitrate):
val = 0;
ret = safe_atoi32(dbmfi->samplerate, &val);
if ((ret < 0) || (val == 0))
val = 1411;
else
val = (val * 8) / 250;
ptr = NULL;
strval = &ptr;
break;
case dbmfi_offsetof(description):
ptr = "wav audio file";
strval = &ptr;
break;
default:
break;
}
}
dmap_add_field(song, df, *strval, val);
dmap_add_field(song, df, *strval, 0);
DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval);
}

View File

@ -79,7 +79,7 @@ void
dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errmsg);
int
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav);
dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags);
int
dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item);

View File

@ -66,10 +66,6 @@
"<h1>%s</h1>\n" \
"</body>\n</html>\n"
#define HTTPD_STREAM_SAMPLE_RATE 44100
#define HTTPD_STREAM_BPS 16
#define HTTPD_STREAM_CHANNELS 2
extern struct httpd_module httpd_dacp;
extern struct httpd_module httpd_daap;
extern struct httpd_module httpd_jsonapi;
@ -93,6 +89,7 @@ static struct httpd_module *httpd_modules[] = {
struct content_type_map {
char *ext;
enum transcode_profile profile;
char *ctype;
};
@ -112,15 +109,18 @@ struct stream_ctx {
static const struct content_type_map ext2ctype[] =
{
{ ".html", "text/html; charset=utf-8" },
{ ".xml", "text/xml; charset=utf-8" },
{ ".css", "text/css; charset=utf-8" },
{ ".txt", "text/plain; charset=utf-8" },
{ ".js", "application/javascript; charset=utf-8" },
{ ".gif", "image/gif" },
{ ".ico", "image/x-ico" },
{ ".png", "image/png" },
{ NULL, NULL }
{ ".html", XCODE_NONE, "text/html; charset=utf-8" },
{ ".xml", XCODE_NONE, "text/xml; charset=utf-8" },
{ ".css", XCODE_NONE, "text/css; charset=utf-8" },
{ ".txt", XCODE_NONE, "text/plain; charset=utf-8" },
{ ".js", XCODE_NONE, "application/javascript; charset=utf-8" },
{ ".gif", XCODE_NONE, "image/gif" },
{ ".ico", XCODE_NONE, "image/x-ico" },
{ ".png", XCODE_PNG, "image/png" },
{ ".jpg", XCODE_JPEG, "image/jpeg" },
{ ".mp3", XCODE_MP3, "audio/mpeg" },
{ ".wav", XCODE_WAV, "audio/wav" },
{ NULL, XCODE_NONE, NULL }
};
static char webroot_directory[PATH_MAX];
@ -173,6 +173,40 @@ scrobble_cb(void *arg)
}
#endif
static const char *
content_type_from_ext(const char *ext)
{
int i;
if (!ext)
return NULL;
for (i = 0; ext2ctype[i].ext; i++)
{
if (strcmp(ext, ext2ctype[i].ext) == 0)
return ext2ctype[i].ctype;
}
return NULL;
}
static const char *
content_type_from_profile(enum transcode_profile profile)
{
int i;
if (profile == XCODE_NONE)
return NULL;
for (i = 0; ext2ctype[i].ext; i++)
{
if (profile == ext2ctype[i].profile)
return ext2ctype[i].ctype;
}
return NULL;
}
/* --------------------------- MODULES INTERFACE ---------------------------- */
@ -424,13 +458,11 @@ httpd_response_not_cachable(struct httpd_request *hreq)
static void
serve_file(struct httpd_request *hreq)
{
char *ext;
char path[PATH_MAX];
char deref[PATH_MAX];
char *ctype;
const char *ctype;
struct stat sb;
int fd;
int i;
uint8_t buf[4096];
bool slashed;
int ret;
@ -544,19 +576,9 @@ serve_file(struct httpd_request *hreq)
goto out_fail;
}
ctype = "application/octet-stream";
ext = strrchr(path, '.');
if (ext)
{
for (i = 0; ext2ctype[i].ext; i++)
{
if (strcmp(ext, ext2ctype[i].ext) == 0)
{
ctype = ext2ctype[i].ctype;
break;
}
}
}
ctype = content_type_from_ext(strrchr(path, '.'));
if (!ctype)
ctype = "application/octet-stream";
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
@ -570,7 +592,6 @@ serve_file(struct httpd_request *hreq)
close(fd);
}
/* ---------------------------- STREAM HANDLING ----------------------------- */
// This will triggered in a httpd thread, but since the reading may be in a
@ -653,10 +674,11 @@ stream_new(struct media_file_info *mfi, struct httpd_request *hreq, event_callba
}
static struct stream_ctx *
stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
stream_new_transcode(struct media_file_info *mfi, enum transcode_profile profile, struct httpd_request *hreq,
int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
{
struct stream_ctx *st;
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, 0 };
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
st = stream_new(mfi, hreq, stream_cb);
if (!st)
@ -664,7 +686,7 @@ stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, in
goto error;
}
st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size);
st->xcode = transcode_setup(profile, &quality, mfi->data_kind, mfi->path, mfi->song_length);
if (!st->xcode)
{
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
@ -673,6 +695,15 @@ stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, in
goto error;
}
st->size = transcode_encode_query(st->xcode->encode_ctx, "estimated_size");
if (st->size < 0)
{
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, could not determine estimated size\n");
httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
goto error;
}
st->stream_size = st->size - offset;
if (end_offset > 0)
st->stream_size -= (st->size - end_offset);
@ -913,12 +944,13 @@ httpd_stream_file(struct httpd_request *hreq, int id)
{
struct media_file_info *mfi = NULL;
struct stream_ctx *st = NULL;
enum transcode_profile profile;
const char *param;
const char *param_end;
const char *ctype;
char buf[64];
int64_t offset = 0;
int64_t end_offset = 0;
int transcode;
int ret;
param = httpd_header_find(hreq->in_headers, "Range");
@ -973,18 +1005,29 @@ httpd_stream_file(struct httpd_request *hreq, int id)
}
param = httpd_header_find(hreq->in_headers, "Accept-Codecs");
profile = transcode_needed(hreq->user_agent, param, mfi->codectype);
if (profile == XCODE_UNKNOWN)
{
DPRINTF(E_LOG, L_HTTPD, "Could not serve '%s' to client, unable to determine output format\n", mfi->path);
transcode = transcode_needed(hreq->user_agent, param, mfi->codectype);
if (transcode)
httpd_send_error(hreq, HTTP_INTERNAL, "Cannot stream, unable to determine output format");
goto error;
}
if (profile != XCODE_NONE)
{
DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path);
st = stream_new_transcode(mfi, hreq, offset, end_offset, stream_chunk_xcode_cb);
st = stream_new_transcode(mfi, profile, hreq, offset, end_offset, stream_chunk_xcode_cb);
if (!st)
goto error;
ctype = content_type_from_profile(profile);
if (!ctype)
goto error;
if (!httpd_header_find(hreq->out_headers, "Content-Type"))
httpd_header_add(hreq->out_headers, "Content-Type", "audio/wav");
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
}
else
{
@ -1027,7 +1070,7 @@ httpd_stream_file(struct httpd_request *hreq, int id)
// If we are not decoding, send the Content-Length. We don't do that if we
// are decoding because we can only guesstimate the size in this case and
// the error margin is unknown and variable.
if (!transcode)
if (profile == XCODE_NONE)
{
ret = snprintf(buf, sizeof(buf), "%" PRIi64, (int64_t)st->size);
if ((ret < 0) || (ret >= sizeof(buf)))
@ -1059,7 +1102,7 @@ httpd_stream_file(struct httpd_request *hreq, int id)
}
#ifdef HAVE_POSIX_FADVISE
if (!transcode)
if (profile == XCODE_NONE)
{
// Hint the OS
if ( (ret = posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_WILLNEED)) != 0 ||

View File

@ -1148,12 +1148,14 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
const char *param;
const char *client_codecs;
const char *tag;
char *last_codectype;
size_t len;
enum transcode_profile profile;
struct transcode_metadata_string xcode_metadata;
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
uint32_t len_ms;
int nmeta = 0;
int sort_headers;
int nsongs;
int transcode;
int ret;
DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist);
@ -1221,30 +1223,31 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
}
nsongs = 0;
last_codectype = NULL;
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
{
nsongs++;
if (!dbmfi.codectype)
// Not sure if the is_remote path is really needed. Note that if you
// change the below you might need to do the same in rsp_reply_playlist()
profile = s->is_remote ? XCODE_WAV : transcode_needed(hreq->user_agent, client_codecs, dbmfi.codectype);
if (profile == XCODE_UNKNOWN)
{
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
transcode = 0;
}
else if (s->is_remote)
else if (profile != XCODE_NONE)
{
transcode = 1;
}
else if (!last_codectype || (strcmp(last_codectype, dbmfi.codectype) != 0))
{
transcode = transcode_needed(hreq->user_agent, client_codecs, dbmfi.codectype);
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
len_ms = 3 * 60 * 1000; // just a fallback default
free(last_codectype);
last_codectype = strdup(dbmfi.codectype);
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
dbmfi.type = xcode_metadata.type;
dbmfi.codectype = xcode_metadata.codectype;
dbmfi.description = xcode_metadata.description;
dbmfi.file_size = xcode_metadata.file_size;
dbmfi.bitrate = xcode_metadata.bitrate;
}
ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers, transcode);
ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers);
if (ret < 0)
{
DPRINTF(E_LOG, L_DAAP, "Failed to encode song metadata\n");
@ -1270,7 +1273,6 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs);
free(last_codectype);
db_query_end(&qp);
if (ret == -100)

View File

@ -36,6 +36,11 @@
#define HTTP_BADGATEWAY 502 /**< received an invalid response from the upstream */
#define HTTP_SERVUNAVAIL 503 /**< the server is not available */
#define HTTPD_STREAM_SAMPLE_RATE 44100
#define HTTPD_STREAM_BPS 16
#define HTTPD_STREAM_CHANNELS 2
#define HTTPD_STREAM_BIT_RATE 320000
struct httpd_request;

View File

@ -407,28 +407,83 @@ rsp_reply_db(struct httpd_request *hreq)
return 0;
}
static int
item_add(xml_node *parent, struct query_params *qp, const char *user_agent, const char *client_codecs, int mode)
{
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
struct db_media_file_info dbmfi;
struct transcode_metadata_string xcode_metadata;
enum transcode_profile profile;
const char *orgcodec = NULL;
uint32_t len_ms;
xml_node *item;
char **strval;
int ret;
int i;
ret = db_query_fetch_file(&dbmfi, qp);
if (ret != 0)
return ret;
profile = transcode_needed(user_agent, client_codecs, dbmfi.codectype);
if (profile == XCODE_UNKNOWN)
{
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
}
else if (profile != XCODE_NONE)
{
orgcodec = dbmfi.codectype;
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
len_ms = 3 * 60 * 1000; // just a fallback default
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
dbmfi.type = xcode_metadata.type;
dbmfi.codectype = xcode_metadata.codectype;
dbmfi.description = xcode_metadata.description;
dbmfi.file_size = xcode_metadata.file_size;
dbmfi.bitrate = xcode_metadata.bitrate;
}
// Now add block with content
item = xml_new_node(parent, "item", NULL);
for (i = 0; rsp_fields[i].field; i++)
{
if (!(rsp_fields[i].flags & mode))
continue;
strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset);
if (!(*strval) || (strlen(*strval) == 0))
continue;
xml_new_node(item, rsp_fields[i].field, *strval);
// In case we are transcoding
if (rsp_fields[i].offset == dbmfi_offsetof(codectype) && orgcodec)
xml_new_node(item, "original_codec", orgcodec);
}
return 0;
}
static int
rsp_reply_playlist(struct httpd_request *hreq)
{
struct query_params qp;
struct db_media_file_info dbmfi;
const char *param;
const char *ua;
const char *client_codecs;
char **strval;
xml_node *xml;
xml_node *response;
xml_node *items;
xml_node *item;
int mode;
int records;
int transcode;
int32_t bitrate;
int i;
int ret;
memset(&qp, 0, sizeof(struct query_params));
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
ret = safe_atoi32(hreq->path_parts[2], &qp.id);
if (ret < 0)
{
@ -485,70 +540,15 @@ rsp_reply_playlist(struct httpd_request *hreq)
rsp_xml_response_new(&xml, &response, 0, "", records, qp.results);
// Add a parent items block (all items), and then one item per file
items = xml_new_node(response, "items", NULL);
/* Items block (all items) */
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
do
{
ua = httpd_header_find(hreq->in_headers, "User-Agent");
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
/* Item block (one item) */
item = xml_new_node(items, "item", NULL);
for (i = 0; rsp_fields[i].field; i++)
{
if (!(rsp_fields[i].flags & mode))
continue;
strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset);
if (!(*strval) || (strlen(*strval) == 0))
continue;
if (!transcode)
{
xml_new_node(item, rsp_fields[i].field, *strval);
continue;
}
switch (rsp_fields[i].offset)
{
case dbmfi_offsetof(type):
xml_new_node(item, rsp_fields[i].field, "wav");
break;
case dbmfi_offsetof(bitrate):
bitrate = 0;
ret = safe_atoi32(dbmfi.samplerate, &bitrate);
if ((ret < 0) || (bitrate == 0))
bitrate = 1411;
else
bitrate = (bitrate * 8) / 250;
xml_new_node_textf(item, rsp_fields[i].field, "%d", bitrate);
break;
case dbmfi_offsetof(description):
xml_new_node(item, rsp_fields[i].field, "wav audio file");
break;
case dbmfi_offsetof(codectype):
xml_new_node(item, rsp_fields[i].field, "wav");
xml_new_node(item, "original_codec", *strval);
break;
default:
xml_new_node(item, rsp_fields[i].field, *strval);
break;
}
}
ret = item_add(items, &qp, hreq->user_agent, client_codecs, mode);
}
while (ret == 0);
if (qp.filter)
free(qp.filter);
free(qp.filter);
if (ret < 0)
{

View File

@ -38,7 +38,7 @@ setup(struct input_source *source)
{
struct transcode_ctx *ctx;
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms);
if (!ctx)
return -1;

View File

@ -304,7 +304,7 @@ setup(struct input_source *source)
free(source->path);
source->path = url;
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms);
if (!ctx)
return -1;

View File

@ -404,7 +404,7 @@ download_xcode_setup(struct download_ctx *download)
if (!xcode->decode_ctx)
goto error;
xcode->encode_ctx = transcode_encode_setup(XCODE_PCM16, NULL, xcode->decode_ctx, NULL, 0, 0);
xcode->encode_ctx = transcode_encode_setup(XCODE_PCM16, NULL, xcode->decode_ctx, 0, 0);
if (!xcode->encode_ctx)
goto error;

View File

@ -311,7 +311,7 @@ encoding_reset(struct media_quality *quality)
profile = quality_to_xcode(&subscription->quality);
if (profile != XCODE_UNKNOWN)
subscription->encode_ctx = transcode_encode_setup(profile, &subscription->quality, decode_ctx, NULL, 0, 0);
subscription->encode_ctx = transcode_encode_setup(profile, &subscription->quality, decode_ctx, 0, 0);
else
DPRINTF(E_LOG, L_PLAYER, "Could not setup resampling to %d/%d/%d for output\n",
subscription->quality.sample_rate, subscription->quality.bits_per_sample, subscription->quality.channels);

View File

@ -1153,7 +1153,7 @@ master_session_make(struct media_quality *quality)
goto error;
}
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, NULL, 0, 0);
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, 0, 0);
transcode_decode_cleanup(&decode_ctx);
if (!rms->encode_ctx)
{

View File

@ -2399,7 +2399,7 @@ cast_init(void)
goto out_tls_deinit;
}
cast_encode_ctx = transcode_encode_setup(XCODE_OPUS, &cast_quality_default, decode_ctx, NULL, 0, 0);
cast_encode_ctx = transcode_encode_setup(XCODE_OPUS, &cast_quality_default, decode_ctx, 0, 0);
transcode_decode_cleanup(&decode_ctx);
if (!cast_encode_ctx)
{

View File

@ -1888,7 +1888,7 @@ master_session_make(struct media_quality *quality, bool encrypt)
goto error;
}
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, NULL, 0, 0);
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, 0, 0);
transcode_decode_cleanup(&decode_ctx);
if (!rms->encode_ctx)
{

View File

@ -133,7 +133,7 @@ encoder_setup(enum player_format format, struct media_quality *quality)
}
if (format == PLAYER_FORMAT_MP3)
encode_ctx = transcode_encode_setup(XCODE_MP3, quality, decode_ctx, NULL, 0, 0);
encode_ctx = transcode_encode_setup(XCODE_MP3, quality, decode_ctx, 0, 0);
if (!encode_ctx)
{

View File

@ -139,8 +139,8 @@ struct decode_ctx
struct stream_ctx audio_stream;
struct stream_ctx video_stream;
// Duration (used to make wav header)
uint32_t duration;
// Source duration in ms as provided by caller
uint32_t len_ms;
// Data kind (used to determine if ICY metadata is relevant to look for)
enum data_kind data_kind;
@ -186,7 +186,10 @@ struct encode_ctx
AVPacket *encoded_pkt;
// How many output bytes we have processed in total
off_t total_bytes;
off_t bytes_processed;
// Estimated total size of output
off_t bytes_total;
// Used to check for ICY metadata changes at certain intervals
uint32_t icy_interval;
@ -240,7 +243,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile, str
settings->with_user_filters = true;
break;
case XCODE_PCM16_HEADER:
case XCODE_WAV:
settings->with_wav_header = true;
settings->with_user_filters = true;
case XCODE_PCM16:
@ -435,32 +438,48 @@ add_le32(uint8_t *dst, uint32_t val)
* header must have size WAV_HEADER_LEN (44 bytes)
*/
static void
make_wav_header(uint8_t *header, off_t *est_size, int sample_rate, int bps, int channels, int duration)
make_wav_header(uint8_t *header, int sample_rate, int bytes_per_sample, int channels, off_t bytes_total)
{
uint32_t wav_len;
if (duration == 0)
duration = 3 * 60 * 1000; /* 3 minutes, in ms */
wav_len = channels * bps * sample_rate * (duration / 1000);
if (est_size)
*est_size = wav_len + WAV_HEADER_LEN;
uint32_t wav_size = bytes_total - WAV_HEADER_LEN;
memcpy(header, "RIFF", 4);
add_le32(header + 4, 36 + wav_len);
add_le32(header + 4, 36 + wav_size);
memcpy(header + 8, "WAVEfmt ", 8);
add_le32(header + 16, 16);
add_le16(header + 20, 1);
add_le16(header + 22, channels); /* channels */
add_le32(header + 24, sample_rate); /* samplerate */
add_le32(header + 28, sample_rate * channels * bps); /* byte rate */
add_le16(header + 32, channels * bps); /* block align */
add_le16(header + 34, 8 * bps); /* bits per sample */
add_le32(header + 28, sample_rate * channels * bytes_per_sample); /* byte rate */
add_le16(header + 32, channels * bytes_per_sample); /* block align */
add_le16(header + 34, 8 * bytes_per_sample); /* bits per sample */
memcpy(header + 36, "data", 4);
add_le32(header + 40, wav_len);
add_le32(header + 40, wav_size);
}
static off_t
size_estimate(enum transcode_profile profile, int bit_rate, int sample_rate, int bytes_per_sample, int channels, int len_ms)
{
off_t bytes;
// If the source has a number of samples that doesn't match an even len_ms
// then the length may have been rounded up. We prefer an estimate that is on
// the low side, otherwise ffprobe won't trust the length from our wav header.
if (len_ms > 0)
len_ms -= 1;
else
len_ms = 3 * 60 * 1000;
if (profile == XCODE_WAV)
bytes = (int64_t)len_ms * channels * bytes_per_sample * sample_rate / 1000 + WAV_HEADER_LEN;
else if (profile == XCODE_MP3)
bytes = (int64_t)len_ms * bit_rate / 8000;
else
bytes = -1;
return bytes;
}
/*
* Checks if this stream index is one that we are decoding
*
@ -515,6 +534,8 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id
return -1;
}
DPRINTF(E_DBG, L_XCODE, "Selected encoder '%s'\n", encoder->long_name);
CHECK_NULL(L_XCODE, s->stream = avformat_new_stream(ctx->ofmt_ctx, NULL));
CHECK_NULL(L_XCODE, s->codec = avcodec_alloc_context3(encoder));
@ -1529,6 +1550,11 @@ open_filters(struct encode_ctx *ctx, struct decode_ctx *src_ctx)
ret = create_filtergraph(&ctx->audio_stream, filters, ARRAY_SIZE(filters), &src_ctx->audio_stream);
if (ret < 0)
goto out_fail;
// Many audio encoders require a fixed frame size. This will ensure that
// the filt_frame from av_buffersink_get_frame has that size (except EOF).
if (! (ctx->audio_stream.codec->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE))
av_buffersink_set_frame_size(ctx->audio_stream.buffersink_ctx, ctx->audio_stream.codec->frame_size);
}
if (ctx->settings.encode_video)
@ -1563,7 +1589,7 @@ close_filters(struct encode_ctx *ctx)
/* Setup */
struct decode_ctx *
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct transcode_evbuf_io *evbuf_io, uint32_t song_length)
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct transcode_evbuf_io *evbuf_io, uint32_t len_ms)
{
struct decode_ctx *ctx;
int ret;
@ -1572,7 +1598,7 @@ transcode_decode_setup(enum transcode_profile profile, struct media_quality *qua
CHECK_NULL(L_XCODE, ctx->decoded_frame = av_frame_alloc());
CHECK_NULL(L_XCODE, ctx->packet = av_packet_alloc());
ctx->duration = song_length;
ctx->len_ms = len_ms;
ctx->data_kind = data_kind;
ret = init_settings(&ctx->settings, profile, quality);
@ -1603,11 +1629,11 @@ transcode_decode_setup(enum transcode_profile profile, struct media_quality *qua
}
struct encode_ctx *
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height)
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, int width, int height)
{
struct encode_ctx *ctx;
int src_bps;
int dst_bps;
int src_bytes_per_sample;
int dst_bytes_per_sample;
int channels;
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct encode_ctx)));
@ -1629,8 +1655,8 @@ transcode_encode_setup(enum transcode_profile profile, struct media_quality *qua
// Caller did not specify a sample format -> determine from source
if (!ctx->settings.sample_format && ctx->settings.encode_audio)
{
src_bps = av_get_bytes_per_sample(src_ctx->audio_stream.codec->sample_fmt);
if (src_bps == 4)
src_bytes_per_sample = av_get_bytes_per_sample(src_ctx->audio_stream.codec->sample_fmt);
if (src_bytes_per_sample == 4)
{
ctx->settings.sample_format = AV_SAMPLE_FMT_S32;
ctx->settings.audio_codec = AV_CODEC_ID_PCM_S32LE;
@ -1663,17 +1689,14 @@ transcode_encode_setup(enum transcode_profile profile, struct media_quality *qua
channels = ctx->settings.channels;
#endif
if (ctx->settings.with_wav_header)
{
dst_bps = av_get_bytes_per_sample(ctx->settings.sample_format);
make_wav_header(ctx->wav_header, est_size, ctx->settings.sample_rate, dst_bps, channels, src_ctx->duration);
}
dst_bytes_per_sample = av_get_bytes_per_sample(ctx->settings.sample_format);
ctx->bytes_total = size_estimate(profile, ctx->settings.bit_rate, ctx->settings.sample_rate, dst_bytes_per_sample, channels, src_ctx->len_ms);
if (ctx->settings.with_wav_header)
make_wav_header(ctx->wav_header, ctx->settings.sample_rate, dst_bytes_per_sample, channels, ctx->bytes_total);
if (ctx->settings.with_icy && src_ctx->data_kind == DATA_KIND_HTTP)
{
dst_bps = av_get_bytes_per_sample(ctx->settings.sample_format);
ctx->icy_interval = METADATA_ICY_INTERVAL * channels * dst_bps * ctx->settings.sample_rate;
}
ctx->icy_interval = METADATA_ICY_INTERVAL * channels * dst_bytes_per_sample * ctx->settings.sample_rate;
if (open_output(ctx, src_ctx) < 0)
goto fail_free;
@ -1693,20 +1716,20 @@ transcode_encode_setup(enum transcode_profile profile, struct media_quality *qua
}
struct transcode_ctx *
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size)
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t len_ms)
{
struct transcode_ctx *ctx;
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct transcode_ctx)));
ctx->decode_ctx = transcode_decode_setup(profile, quality, data_kind, path, NULL, song_length);
ctx->decode_ctx = transcode_decode_setup(profile, quality, data_kind, path, NULL, len_ms);
if (!ctx->decode_ctx)
{
free(ctx);
return NULL;
}
ctx->encode_ctx = transcode_encode_setup(profile, quality, ctx->decode_ctx, est_size, 0, 0);
ctx->encode_ctx = transcode_encode_setup(profile, quality, ctx->decode_ctx, 0, 0);
if (!ctx->encode_ctx)
{
transcode_decode_cleanup(&ctx->decode_ctx);
@ -1779,87 +1802,73 @@ transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality
return NULL;
}
int
enum transcode_profile
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype)
{
char *codectype;
cfg_t *lib;
int size;
bool force_xcode;
int count;
int i;
if (!file_codectype)
{
DPRINTF(E_LOG, L_XCODE, "Can't determine decode status, codec type is unknown\n");
return -1;
return XCODE_UNKNOWN;
}
lib = cfg_getsec(cfg, "library");
size = cfg_size(lib, "no_decode");
if (size > 0)
count = cfg_size(lib, "no_decode");
for (i = 0; i < count; i++)
{
for (i = 0; i < size; i++)
{
codectype = cfg_getnstr(lib, "no_decode", i);
if (strcmp(file_codectype, codectype) == 0)
return 0; // Codectype is in no_decode
}
codectype = cfg_getnstr(lib, "no_decode", i);
if (strcmp(file_codectype, codectype) == 0)
return XCODE_NONE; // Codectype is in no_decode
}
size = cfg_size(lib, "force_decode");
if (size > 0)
count = cfg_size(lib, "force_decode");
for (i = 0, force_xcode = false; i < count && !force_xcode; i++)
{
for (i = 0; i < size; i++)
{
codectype = cfg_getnstr(lib, "force_decode", i);
codectype = cfg_getnstr(lib, "force_decode", i);
if (strcmp(file_codectype, codectype) == 0)
force_xcode = true; // Codectype is in force_decode
}
if (strcmp(file_codectype, codectype) == 0)
return 1; // Codectype is in force_decode
}
if (!client_codecs && user_agent)
{
if (strncmp(user_agent, "iTunes", strlen("iTunes")) == 0)
client_codecs = itunes_codecs;
else if (strncmp(user_agent, "Music/", strlen("Music/")) == 0) // Apple Music, include slash because the name is generic
client_codecs = itunes_codecs;
else if (strncmp(user_agent, "QuickTime", strlen("QuickTime")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "Front%20Row", strlen("Front%20Row")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "AppleCoreMedia", strlen("AppleCoreMedia")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "Roku", strlen("Roku")) == 0)
client_codecs = roku_codecs;
else if (strncmp(user_agent, "Hifidelio", strlen("Hifidelio")) == 0)
/* 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 XCODE_NONE;
}
if (!client_codecs)
{
if (user_agent)
{
if (strncmp(user_agent, "iTunes", strlen("iTunes")) == 0)
client_codecs = itunes_codecs;
else if (strncmp(user_agent, "Music/", strlen("Music/")) == 0) // Apple Music, include slash because the name is generic
client_codecs = itunes_codecs;
else if (strncmp(user_agent, "QuickTime", strlen("QuickTime")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "Front%20Row", strlen("Front%20Row")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "AppleCoreMedia", strlen("AppleCoreMedia")) == 0)
client_codecs = itunes_codecs; // Use iTunes codecs
else if (strncmp(user_agent, "Roku", strlen("Roku")) == 0)
client_codecs = roku_codecs;
else if (strncmp(user_agent, "Hifidelio", strlen("Hifidelio")) == 0)
/* 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;
}
}
client_codecs = default_codecs;
else
DPRINTF(E_SPAM, L_XCODE, "Client advertises codecs: %s\n", client_codecs);
if (!client_codecs)
{
DPRINTF(E_SPAM, L_XCODE, "Could not identify client, using default codectype set\n");
client_codecs = default_codecs;
}
if (strstr(client_codecs, file_codectype))
{
DPRINTF(E_SPAM, L_XCODE, "Codectype supported by client, no decoding needed\n");
return 0;
}
DPRINTF(E_SPAM, L_XCODE, "Will decode\n");
return 1;
if (!force_xcode && strstr(client_codecs, file_codectype))
return XCODE_NONE;
else if (strstr(client_codecs, "mpeg"))
return XCODE_MP3;
else if (strstr(client_codecs, "wav"))
return XCODE_WAV;
else
return XCODE_UNKNOWN;
}
@ -2010,9 +2019,9 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int
evbuffer_add_buffer(evbuf, ctx->encode_ctx->obuf);
ctx->encode_ctx->total_bytes += processed;
ctx->encode_ctx->bytes_processed += processed;
if (icy_timer && ctx->encode_ctx->icy_interval)
*icy_timer = (ctx->encode_ctx->total_bytes % ctx->encode_ctx->icy_interval < processed);
*icy_timer = (ctx->encode_ctx->bytes_processed % ctx->encode_ctx->icy_interval < processed);
if ((ret < 0) && (ret != AVERROR_EOF))
return ret;
@ -2218,6 +2227,11 @@ transcode_encode_query(struct encode_ctx *ctx, const char *query)
if (ctx->audio_stream.stream)
return ctx->audio_stream.stream->codecpar->frame_size;
}
else if (strcmp(query, "estimated_size") == 0)
{
if (ctx->audio_stream.stream)
return ctx->bytes_total;
}
return -1;
}
@ -2244,3 +2258,38 @@ transcode_metadata(struct transcode_ctx *ctx, int *changed)
return m;
}
void
transcode_metadata_strings_set(struct transcode_metadata_string *s, enum transcode_profile profile, struct media_quality *q, uint32_t len_ms)
{
off_t bytes;
memset(s, 0, sizeof(struct transcode_metadata_string));
switch (profile)
{
case XCODE_WAV:
s->type = "wav";
s->codectype = "wav";
s->description = "WAV audio file";
snprintf(s->bitrate, sizeof(s->bitrate), "%d", 8 * STOB(q->sample_rate, q->bits_per_sample, q->channels) / 1000); // 44100/16/2 -> 1411
bytes = size_estimate(profile, q->bit_rate, q->sample_rate, q->bits_per_sample / 8, q->channels, len_ms);
snprintf(s->file_size, sizeof(s->file_size), "%d", (int)bytes);
break;
case XCODE_MP3:
s->type = "mp3";
s->codectype = "mpeg";
s->description = "MPEG audio file";
snprintf(s->bitrate, sizeof(s->bitrate), "%d", q->bit_rate / 1000);
bytes = size_estimate(profile, q->bit_rate, q->sample_rate, q->bits_per_sample / 8, q->channels, len_ms);
snprintf(s->file_size, sizeof(s->file_size), "%d", (int)bytes);
break;
default:
DPRINTF(E_WARN, L_XCODE, "transcode_metadata_strings_set() called with unknown profile %d\n", profile);
}
}

View File

@ -10,11 +10,13 @@
enum transcode_profile
{
// Used for errors
XCODE_UNKNOWN = 0,
XCODE_UNKNOWN,
// No transcoding, send as-is
XCODE_NONE,
// Decodes the best audio stream into PCM16 or PCM24, no resampling (does not add wav header)
XCODE_PCM_NATIVE,
// Decodes/resamples the best audio stream into PCM16 (with wav header)
XCODE_PCM16_HEADER,
XCODE_WAV,
// Decodes/resamples the best audio stream into PCM16/24/32 (no wav headers)
XCODE_PCM16,
XCODE_PCM24,
@ -60,20 +62,30 @@ struct transcode_evbuf_io
void *seekfn_arg;
};
struct transcode_metadata_string
{
char *type;
char *codectype;
char *description;
char file_size[64];
char bitrate[32];
};
// Setting up
struct decode_ctx *
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct transcode_evbuf_io *evbuf_io, uint32_t song_length);
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct transcode_evbuf_io *evbuf_io, uint32_t len_ms);
struct encode_ctx *
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height);
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, int width, int height);
struct transcode_ctx *
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size);
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t len_ms);
struct decode_ctx *
transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality);
int
enum transcode_profile
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
// Cleaning up
@ -170,4 +182,10 @@ transcode_encode_query(struct encode_ctx *ctx, const char *query);
struct http_icy_metadata *
transcode_metadata(struct transcode_ctx *ctx, int *changed);
// When transcoding, we are in essence serving a different source file than the
// original to the client. So we can't serve some of the file metadata from the
// filescanner. This function creates strings to be used for override.
void
transcode_metadata_strings_set(struct transcode_metadata_string *s, enum transcode_profile profile, struct media_quality *q, uint32_t len_ms);
#endif /* !__TRANSCODE_H__ */