/* ** mod_annodex.c -- Apache annodex module ** [originally autogenerated via ``apxs -n annodex -g''] ** */ #include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "ap_config.h" #include "http_core.h" #include "http_log.h" #include "apr_strings.h" #include "apr_uri.h" #include "apr_file_info.h" #include #include #include /* chdir */ #include #include #include /* define __PROTOTYPE to enable mode= parameter */ /* #define __PROTOTYPE */ #define DEBUG #define ANX_MIME_TYPE "application/annodex" #define CMML_MIME_TYPE "text/cmml" #define CMML_PREAMBLE \ "\n" #define MOD_ANNODEX_TYPE_UNKNOWN -1 #define MOD_ANNODEX_TYPE_ANX 0 #define MOD_ANNODEX_TYPE_CMML 1 #ifdef __PROTOTYPE #define MOD_ANNODEX_TYPE_MEDIA 2 #endif #define MAX_ANCHOR_LEN 32768 #define MEDIA_BUF_LEN 102400 /** * Print a time as npt using ap_rprintf * \param r the request record * \param seconds the time to print * \returns the ap_rprintf return value */ static int rprintf_time_npt (request_rec * r, double seconds) { int hrs, min; double sec; char * sign; sign = (seconds < 0.0) ? "-" : ""; if (seconds < 0.0) seconds = -seconds; hrs = (int) (seconds/3600.0); min = (int) ((seconds - ((double)hrs * 3600.0)) / 60.0); sec = seconds - ((double)hrs * 3600.0)- ((double)min * 60.0); /* XXX: %02.3f workaround */ if (sec < 10.0) { return ap_rprintf (r, "%s%02d:%02d:0%2.3f", sign, hrs, min, sec); } else { return ap_rprintf (r, "%s%02d:%02d:%02.3f", sign, hrs, min, sec); } } /** * Create a table corresponding to name=value pairs in the query string * @param r The resource request * @param query The query string * @return A newly created table with corresponding name=value keys. */ static apr_table_t * make_cgi_table (request_rec * r, char * query) { apr_table_t * t; char * key, * val, * end; t = apr_table_make (r->pool, 3); if (!query) return t; key = query; do { val = strchr (key, '='); end = strchr (key, '&'); if (end) { if (val) { if (val < end) { *val++ = '\0'; } else { val = NULL; } } *end++ = '\0'; } else { if (val) *val++ = '\0'; } /*ap_rprintf (r, "%s = %s\n", key, val);*/ apr_table_set (t, key, val); key = end; } while (end != NULL); return t; } /** * Determine the relative quality factor of a mime type given the Accept: * header of a resource request, following rules of RFC2616 Sec. 14.1 * @param r The resource request * @param content_type The content_type to check * @return The relative quality factor */ static float get_accept_quality (request_rec * r, char * content_type) { char * a, * accept, *next, * last, * pnext, * plast; float q = 0.0, type_q = 0.0, all_q = 0.0; char * m_sep, * m_major; apr_size_t m_major_len; a = (char *)apr_table_get (r->headers_in, (const char *)"Accept"); /* If there was no Accept: header, accept all types equally */ if (a == NULL) return 1.0; /* Form a 'major / *' mime type range for later comparison */ m_sep = strchr (content_type, '/'); m_major_len = (apr_size_t)(m_sep - content_type); m_major = apr_pstrndup (r->pool, content_type, m_major_len + 2); *(m_major+m_major_len+1) = '*'; *(m_major+m_major_len+2) = '\0'; /* Copy the Accept line for tokenization */ accept = apr_pstrdup (r->pool, a); apr_collapse_spaces (accept, accept); next = apr_strtok (accept, ",", &last); while (next) { pnext = apr_strtok (next, ";", &plast); if (!strcmp (pnext, content_type)) { while (pnext) { pnext = apr_strtok (NULL, ";", &plast); if (pnext && sscanf (pnext, "q=%f", &q) == 1) { return q; } } return 1.0; } else if (!strcmp (pnext, "*/*")) { while (pnext) { pnext = apr_strtok (NULL, ";", &plast); if (pnext && sscanf (pnext, "q=%f", &q) == 1) { all_q = q; } } all_q = 1.0; } else if (!strcmp (pnext, m_major)) { while (pnext) { pnext = apr_strtok (NULL, ";", &plast); if (pnext && sscanf (pnext, "q=%f", &q) == 1) { type_q = q; } } type_q = 1.0; } next = apr_strtok (NULL, ",", &last); } if (q > 0.0) return q; else if (type_q > 0.0) return type_q; else return all_q; } #ifdef __PROTOTYPE static int get_cgi_mode (request_rec * r, apr_table_t * cgi_table) { char * cgi_mode; cgi_mode = (char *)apr_table_get (cgi_table, "mode"); if (cgi_mode == NULL) return MOD_ANNODEX_TYPE_UNKNOWN; #ifdef DEBUG ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "cgi mode %s", cgi_mode); #endif if (!strcmp (cgi_mode, "cmml")) return MOD_ANNODEX_TYPE_CMML; else if (!strcmp (cgi_mode, "media")) return MOD_ANNODEX_TYPE_MEDIA; else return MOD_ANNODEX_TYPE_UNKNOWN; } #endif /* __PROTOTYPE */ static int prefer_cmml (request_rec * r) { float qc, qa; qc = get_accept_quality (r, CMML_MIME_TYPE); qa = get_accept_quality (r, ANX_MIME_TYPE); #ifdef DEBUG ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Accept CMML %f, Accept ANX %f\n", qc, qa); #endif return (qc > qa); } /** * Parse the first time range in a string; multiple time ranges may be * separated by commas. * @param s The string to parse. * @param range_start The location to store the start of the range. * @param range_end The location to store the end of the range. * @return a pointer to the start of the next time range, or NULL if * no further ranges are specified in s. */ static char * ma_parse_time (char * s, double * range_start, double * range_end) { char * sep, * end; sep = strchr (s, '/'); end = strchr (s, ','); if (end != NULL) { /* If we found the separator for the following range, then * ignore it while processing this range. */ if (sep && end < sep) sep = NULL; /* Terminate the range for string processing */ *end++ = '\0'; } if (sep == NULL) { *range_start = anx_parse_time (s); *range_end = -1.0; } else { /* Terminate the range start */ *sep++ = '\0'; *range_start = anx_parse_time (s); *range_end = anx_parse_time (sep); } return end; } static double ma_anxenc_clip_time (char * filename, char * content_type, char * id) { ANNODEX * anx; int ret; unsigned char buf[MEDIA_BUF_LEN]; long n = MEDIA_BUF_LEN; double clip_offset; anx = anx_new (ANX_WRITE); ret = anx_write_import (anx, filename, NULL, content_type, 0.0, -1.0, 0); /* Only spin through the file if the requested clip has not yet been * inserted; avoid scanning media if 'filename' is a cmml file. */ if ((clip_offset = anx_get_clip_time_by_id (anx, id)) == -1.0) { while ((n = anx_write_output (anx, buf, MEDIA_BUF_LEN)) > 0); if ((clip_offset = anx_get_clip_time_by_id (anx, id)) == -1.0) { clip_offset = 0.0; } } if (anx_close (anx) != NULL) { } return clip_offset; } static int ma_anxenc (request_rec * r, char * filename, char * content_type, apr_table_t * cgi_table) { ANNODEX * anx; int ret; char * val, * id = NULL; double seek_offset = 0.0, seek_end = -1.0; char buf[MEDIA_BUF_LEN]; long n = MEDIA_BUF_LEN; double header_value; char * path_sep, olddir[APR_PATH_MAX], * newdir, * sep_pos; anx = anx_new (ANX_WRITE); anx_init_importers ("*/*"); /* Retrieve the current working directory */ getcwd (olddir, APR_PATH_MAX); /* Chdir to the directory of the requested file */ apr_filepath_get (&path_sep, APR_FILEPATH_NATIVE, r->pool); newdir = apr_pstrdup (r->pool, filename); sep_pos = strrchr (newdir, path_sep[0]); if (sep_pos) *sep_pos = '\0'; chdir (newdir); /* Get the start/end times or id */ val = (char *)apr_table_get (cgi_table, "t"); id = (char *)apr_table_get (cgi_table, "id"); if (val) { ma_parse_time (val, &seek_offset, &seek_end); } else if (id) { seek_offset = ma_anxenc_clip_time (filename, content_type, id); } #ifdef DEBUG ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "ma_anxenc t=%s id=%s (%f/%f)", val, id, seek_offset, seek_end); #endif anx_set_presentation_time (anx, seek_offset); anx_set_basetime (anx, 0.0); ret = anx_write_import (anx, filename, NULL, content_type, seek_offset, seek_end, 0); if ((header_value = anx_get_duration (anx)) != -1.0) { apr_table_set (r->headers_out, (const char *)"X-Content-Duration", apr_ltoa (r->pool, (long)header_value)); } if ((header_value = anx_get_bitrate (anx)) != -1.0) { apr_table_set (r->headers_out, (const char *)"X-Content-Bitrate-Average", apr_ltoa (r->pool, (long)header_value)); } while ((n = anx_write_output (anx, buf, MEDIA_BUF_LEN)) > 0) { ap_rwrite (buf, n, r); } if (anx_close (anx) != NULL) { } /* Return to the previously current working directory */ chdir (olddir); return 0; } /* ma_anxrip callbacks */ struct ma_anxrip_data_t { request_rec * r; AnxClip * prev_clip; double prev_start_time; }; static int read_head (ANNODEX * anx, const AnxHead * head, void * user_data) { struct ma_anxrip_data_t * mad = (struct ma_anxrip_data_t *)user_data; request_rec * r = mad->r; char buf[MAX_ANCHOR_LEN]; anx_head_snprint (buf, MAX_ANCHOR_LEN, (AnxHead *)head); ap_rputs (buf, r); ap_rputc ('\n', r); return ANX_CONTINUE; } static int read_clip (ANNODEX * anx, const AnxClip * clip, void * user_data) { struct ma_anxrip_data_t * mad = (struct ma_anxrip_data_t *)user_data; request_rec * r = mad->r; char buf[MAX_ANCHOR_LEN]; double t; t = anx_tell_time (anx); /* fprintf (outfile, "%.3f (%ld)\t", t, anx_tell (anx));*/ if (mad->prev_clip != NULL) { anx_clip_snprint (buf, MAX_ANCHOR_LEN, mad->prev_clip, mad->prev_start_time, t); ap_rputs (buf, r); ap_rputc ('\n', r); anx_clip_free (mad->prev_clip); } mad->prev_clip = anx_clip_clone ((AnxClip *)clip); mad->prev_start_time = t; return ANX_CONTINUE; } static int ma_anxrip (request_rec * r, char * filename) { ANNODEX * anx; char buf[MAX_ANCHOR_LEN]; apr_size_t n = MAX_ANCHOR_LEN; double timebase; struct ma_anxrip_data_t mad; mad.r = r; mad.prev_clip = NULL; anx = anx_open (filename, ANX_READ); anx_set_read_head_callback (anx, read_head, &mad); anx_set_read_clip_callback (anx, read_clip, &mad); ap_rprintf (r, CMML_PREAMBLE); ap_rprintf (r, "\n"); ap_rprintf (r, "\n"); /* XXX: and UTC */ #if 0 for (l = anx_get_track_list (anx); l; l = l->next) { s = (AnxTrack *)l->data; if (!(s->content_type && !strncmp (s->content_type, CMML_MIME_TYPE, 11))) { ap_rprintf (r, "id) ap_rprintf (r, " id=\"%s\"", s->id); if (s->content_type) ap_rprintf (r, " contenttype=\"%s\"", s->content_type); /* if (s->granule_rate_n != 0 && s->granule_rate_d != 0) ap_rprintf (r, " granulerate=\"%ld/%ld\"", (long)s->granule_rate_n, (long)s->granule_rate_d); */ ap_rprintf (r, "/>\n"); } } #endif ap_rprintf (r, "\n"); #if 0 head = anx_get_head (anx); anx_head_snprint (buf, MAX_ANCHOR_LEN, (AnxHead *)head); ap_rputs (buf, r); ap_rputc ('\n', r); #endif while ((n = anx_read (anx, 1024)) > 0); if (mad.prev_clip != NULL) { anx_clip_snprint (buf, MAX_ANCHOR_LEN, mad.prev_clip, mad.prev_start_time, anx_tell_time (anx)); ap_rputs (buf, r); ap_rputc ('\n', r); anx_clip_free (mad.prev_clip); } ap_rprintf (r, "\n"); if (anx_close (anx) != NULL) { /*exit_err ("Failed close of annodex");*/ } return 0; } #ifdef __PROTOTYPE /* ma_anxrip_media callbacks */ static double s_seek_offset = 0.0; static int read_m_stream (ANNODEX * anx, double timebase, char * utc, void * user_data) { return ANX_CONTINUE; } static int read_m_track (ANNODEX * anx, long serialno, char * id, char * content_type, anx_int64_t granule_rate_n, anx_int64_t granule_rate_d, int nr_header_packets, void * user_data) { request_rec * r = (request_rec *)user_data; if (!(content_type && !strncmp (content_type, CMML_MIME_TYPE, 11))) { r->content_type = content_type; #ifdef DEBUG ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Rip media: mime type %s\n", r->content_type); #endif } return ANX_CONTINUE; } static int read_media (ANNODEX * anx, unsigned char * data, long len, long serialno, anx_int64_t granulepos, void * user_data) { request_rec * r = (request_rec *)user_data; apr_size_t remaining = (apr_size_t)len, n; #if 0 ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "%f\t%f\n", anx_tell_time (anx), s_seek_offset); if (anx_tell_time (anx) >= s_seek_offset) #endif while (remaining > 0) { n = ap_rwrite (data, remaining, r); remaining -= n; } return ANX_CONTINUE; } static int read_media_1 (ANNODEX * anx, unsigned char * data, long len, long serialno, anx_int64_t granulepos, void * user_data) { request_rec * r = (request_rec *)user_data; r->content_type = anx_track_get_content_type (anx, serialno); read_media (anx, data, len, serialno, granulepos, user_data); anx_set_read_raw_callback (anx, read_media, user_data); return ANX_CONTINUE; } static int ma_anxrip_media (request_rec * r, char * filename, apr_table_t * cgi_table) { ANNODEX * anx; apr_file_t * af; char buf[MAX_ANCHOR_LEN]; apr_size_t n = MAX_ANCHOR_LEN; double seek_offset = 0.0; char * val; val = (char *)apr_table_get (cgi_table, "t"); if (val) { seek_offset = anx_parse_time (val); } anx = anx_open (filename, ANX_READ); anx_set_read_stream_callback (anx, read_m_stream, r); anx_set_read_track_callback (anx, read_m_track, r); anx_set_read_raw_callback (anx, read_media_1, r); if (seek_offset > 0.0) anx_seek_time (anx, seek_offset, ANX_SEEK_SET); /*s_seek_offset = seek_offset;*/ while ((n = anx_read (anx, MEDIA_BUF_LEN)) > 0); if (anx_close (anx) != NULL) { /*exit_err ("Failed close of annodex");*/ } } #endif #ifdef UNUSED static int ma_send (request_rec * r, char * filename) { apr_file_t * af; char buf[MEDIA_BUF_LEN]; apr_size_t n = MEDIA_BUF_LEN; #ifdef DEBUG ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "ma_send: %s", filename); #endif apr_file_open (&af, filename, APR_READ, APR_OS_DEFAULT, r->pool); while (apr_file_read (af, (void *)buf, &n) == APR_SUCCESS) { ap_rwrite (buf, n, r); } apr_file_close (af); return 0; } #endif static int read_cmml_stream (CMML * cmml, const CMML_Stream * stream, void * user_data) { request_rec * r = (request_rec *)user_data; CMML_Preamble * preamble; static char buf[1024]; int n; preamble = cmml_get_preamble (cmml); n = cmml_preamble_snprint (buf, 1024, preamble); if (n > 0) { ap_rwrite (buf, n, r); } return 0; } static int read_cmml_head (CMML * cmml, const CMML_Head * head, void * user_data) { request_rec * r = (request_rec *)user_data; static char buf[MAX_ANCHOR_LEN]; int n; n = cmml_head_pretty_snprint (buf, MAX_ANCHOR_LEN, (CMML_Head *)head); if (n > 0) { ap_rwrite (buf, n, r); } return 0; } static int read_cmml_clip (CMML * cmml, const CMML_Clip * clip, void * user_data) { request_rec * r = (request_rec *)user_data; static char buf[MAX_ANCHOR_LEN]; int n; n = cmml_clip_pretty_snprint (buf, MAX_ANCHOR_LEN, (CMML_Clip *)clip); if (n > 0) { ap_rwrite (buf, n, r); } return 0; } static int ma_send_cmml (request_rec * r, char * filename) { CMML * doc; size_t nread = 0; long n; static char * cmml_tail = "\n"; doc = cmml_open (filename); cmml_set_read_callbacks (doc, read_cmml_stream, read_cmml_head, read_cmml_clip, r); while ((n = cmml_read (doc, 1024)) > 0) nread += n; if (n < 0) { /* ERROR */ } n = strlen (cmml_tail); if (n > 0) { ap_rwrite (cmml_tail, n, r); } cmml_destroy (doc); return 0; } static char * ma_extsub (request_rec * r, char * filename, char * oldext, int oldext_len, char * newext, int newext_len) { char * extpos; char * srcfile; int filename_len; int diff; /* how many bytes longer the new extension is */ char * ret; extpos = strrchr (r->filename, '.'); if (extpos == NULL) return NULL; if (strncmp (++extpos, oldext, oldext_len)) return NULL; /* s/.oldext/.newext/ */ diff = newext_len - oldext_len; filename_len = strlen (filename); srcfile = apr_palloc (r->pool, filename_len + diff + 1 /* '\0' */); apr_cpystrn (srcfile, filename, filename_len-oldext_len+1); apr_cpystrn (srcfile+filename_len-oldext_len, newext, newext_len+1); ret = r->path_info ? apr_pstrcat (r->pool, srcfile, r->path_info, NULL) : srcfile; return ret; } /* The annodex content handler */ static int annodex_handler(request_rec *r) { apr_uri_t * uri; char * filename; char * srcpath = NULL, * cmmlpath = NULL; int input_type, output_type; apr_table_t * cgi_table; uri = &(r->parsed_uri); /* usually filename is request filename */ filename = r->filename; /* but we still need to determine the output type */ output_type = MOD_ANNODEX_TYPE_UNKNOWN; if (strcmp(r->handler, "annodex") && strcmp(r->handler, "application/annodex") && strcmp(r->handler, "application/x-annodex")) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "handler not annodex but %s, DECLINED", r->handler); return DECLINED; } if (r->method_number == M_OPTIONS) { r->allowed = (1 << M_GET); ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "M_OPTIONS, DECLINED"); return DECLINED; } if (r->method_number != M_GET) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "only GET allowed, METHOD_NOT_ALLOWED"); return HTTP_METHOD_NOT_ALLOWED; } /* check if file exists */ if (r->finfo.filetype == APR_NOFILE) { apr_file_t * f; if ((srcpath = ma_extsub (r, r->filename, "anx", 3, "cmml", 4))) { if (apr_file_open (&f, srcpath, APR_READ, APR_OS_DEFAULT, r->pool) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Requested file does not exist, nor does CMML %s", srcpath); return HTTP_NOT_FOUND; } input_type = MOD_ANNODEX_TYPE_CMML; filename = srcpath; } else if ((srcpath = ma_extsub (r, r->filename, "cmml", 4, "anx", 3))) { if (apr_file_open (&f, srcpath, APR_READ, APR_OS_DEFAULT, r->pool) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Requested file %s does not exist, nor does ANX %s", r->filename, srcpath); return HTTP_NOT_FOUND; } input_type = MOD_ANNODEX_TYPE_ANX; output_type = MOD_ANNODEX_TYPE_CMML; filename = srcpath; } else { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Requested file does not exist: %s", (r->path_info ? apr_pstrcat(r->pool, r->filename, r->path_info, NULL) : r->filename)); return HTTP_NOT_FOUND; } } else { apr_file_t * f; if ((cmmlpath = ma_extsub (r, r->filename, "anx", 3, "cmml", 4))) { if (apr_file_open (&f, cmmlpath, APR_READ, APR_OS_DEFAULT, r->pool) != APR_SUCCESS) { cmmlpath = NULL; } else { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "Using stored CMML file %s", cmmlpath); } } input_type = MOD_ANNODEX_TYPE_ANX; } apr_table_set (r->headers_out, (const char *)"X-Accept-TimeURI", ANX_MIME_TYPE); if (r->header_only) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, "header_only, OK"); return OK; } cgi_table = make_cgi_table (r, uri->query); #ifdef __PROTOTYPE if (output_type == MOD_ANNODEX_TYPE_UNKNOWN) output_type = get_cgi_mode (r, cgi_table); if (output_type == MOD_ANNODEX_TYPE_CMML) { r->content_type = CMML_MIME_TYPE; } #endif if (output_type == MOD_ANNODEX_TYPE_UNKNOWN) { if (prefer_cmml (r)) { output_type = MOD_ANNODEX_TYPE_CMML; } else { output_type = MOD_ANNODEX_TYPE_ANX; } } if (output_type == MOD_ANNODEX_TYPE_CMML) { r->content_type = CMML_MIME_TYPE; } else { r->content_type = ANX_MIME_TYPE; } if (input_type == MOD_ANNODEX_TYPE_ANX && output_type == MOD_ANNODEX_TYPE_ANX) { /* anx import anx */ ma_anxenc (r, r->filename, ANX_MIME_TYPE, cgi_table); } else if (input_type == MOD_ANNODEX_TYPE_CMML && output_type == MOD_ANNODEX_TYPE_ANX) { /* anx import cmml */ ma_anxenc (r, srcpath, CMML_MIME_TYPE, cgi_table); } else if (input_type == MOD_ANNODEX_TYPE_ANX && output_type == MOD_ANNODEX_TYPE_CMML) { if (cmmlpath != NULL) { /* push out cmml */ ma_send_cmml (r, cmmlpath); } else { /* anx rip cmml */ ma_anxrip (r, filename); } } else if (input_type == MOD_ANNODEX_TYPE_CMML && output_type == MOD_ANNODEX_TYPE_CMML) { /* push out cmml */ ma_send_cmml (r, srcpath); #ifdef __PROTOTYPE } else if (input_type == MOD_ANNODEX_TYPE_ANX && output_type == MOD_ANNODEX_TYPE_MEDIA) { r->content_type = "video/mpeg"; ma_anxrip_media (r, r->filename, cgi_table); #endif } return OK; } static void annodex_register_hooks(apr_pool_t *p) { ap_hook_handler(annodex_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA annodex_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ annodex_register_hooks /* register hooks */ };