/* * kissdx - KiSS PC-Link Daemon eXtended (based on kissd) * * This file is Copyright (C) 2007 Vidar Tysse * Portions Copyright (C) 2009 Olivier Kahn * This file is Public domain. * * ************************************************************************ * * backtoback.c * * Purpose: Provide functions for accessing a collection of streamable * media files as a larger combined file which is a * concatenation of the individual media files. * This is called back-to-back playback. * * Description: See specification in DevDocs/BackToBackAudioPlayback-spec.txt * * ************************************************************************ */ #include #include #include #include #include #include #include #include #include #include "kissdx.h" #include "utils.h" #include "sendfile.h" #include "backtoback.h" #include "config.h" typedef struct b2b_mediafile_s { char * filename; // Media filename, NOT including path off_t offset; // Offset of start of media file in combined file u_int64_t size; // Size of media file, reduced by size of ID tag if present off_t mediafile_offset; // Offset of start of media content in media file (if ID tag at start of file) } b2b_mediafile_t; typedef struct b2b_combinedfile_s { b2b_mediafile_t** mediafiles; // Array of media files in sequence int mediafilecount; // Number of media files in array u_int64_t size; // Combined size of all media files, excluding skipped ID tags/metadata } b2b_combinedfile_t; typedef struct active_mediafile_s { b2b_mediafile_t* mediafile_p; // Points to current media file in the array in b2b_combinedfile_t int index; // Index of current media file in the array in b2b_combinedfile_t int fd; // File descriptor of the currently open media file } active_mediafile_t; // ----------------------------------------------------------------------------------- // MP3 specific structures etc. Maybe this should go in a separate header some day... #define ID3V2_FLAG_Unsynchronisation ( 1 << 7 ) #define ID3V2_FLAG_Extended_header ( 1 << 6 ) #define ID3V2_FLAG_Experimental_indicator ( 1 << 5 ) #define ID3V2_FLAG_Footer_present ( 1 << 4 ) #define ID3V2_VALUE_EYECATCHER "ID3" #define ID3V1_VALUE_EYECATCHER "TAG" #pragma pack(1) typedef struct syncsafe_int_s { unsigned char byte1; // First byte, most significant unsigned char byte2; // Second byte unsigned char byte3; // Third byte unsigned char byte4; // Fourth byte, least significant } syncsafe_int_t; typedef struct id3v2_header_s { char eyecatcher[3]; // Always ID3V2_VALUE_EYECATCHER unsigned char version_major; // Major ID3 tag version, e.g. 4 == ID3v2.4 unsigned char version_minor; // Minor ID3 tag version, typically 0 unsigned char flags; // Flags syncsafe_int_t size; // 32-bit sync-safe (7bit-per-byte) integer containing tag size } id3v2_header_t; #pragma pack() // ----------------------------------------------------------------------------------- static char Cached_B2BFileName[PATH_MAX] = ""; static b2b_combinedfile_t Cached_CombinedFile; static char B2BPath[PATH_MAX] = ""; static char B2BExt[16] = ""; static active_mediafile_t active_mediafile; /******* Private functions *******/ // ============================================================================== static void free_Cached_CombinedFile(void) { int i; b2b_mediafile_t* mediafile_p; if (Cached_CombinedFile.mediafiles) { loglevel(LOGDEBUG,"Debug: B2B Combined file freeing memory for %s", Cached_B2BFileName); for (i = 0; i < Cached_CombinedFile.mediafilecount; i++) if ((mediafile_p = Cached_CombinedFile.mediafiles[i])) { if (mediafile_p->filename) free(mediafile_p->filename); free(mediafile_p); } free(Cached_CombinedFile.mediafiles); } memset(&Cached_CombinedFile, 0, sizeof Cached_CombinedFile); } // ================================================================= static int decode_syncsafe_int(syncsafe_int_t *ssi) { return ((int)ssi->byte4) | ((int)ssi->byte3) << 7 | ((int)ssi->byte2) << 14 | ((int)ssi->byte1) << 21; } // ======================================================================== static void adjust_for_mp3_id_tags(b2b_mediafile_t *mediafile_entry_p, int fd, int num_files, int file_no) { id3v2_header_t id3v2_header; /* Exemple of content of mp3 file * olivier@DEV:/mnt/unsecure/media4kissdx/audio/Colbie Caillat/Coco$ od -N 127 -c 01\ Oxygen.mp3 * 0000000 I D 3 002 \0 \0 \0 \0 021 f T T 2 \0 \0 \b * 0000020 \0 O x y g e n \0 T P 1 \0 \0 020 \0 C * 0000040 o l b i e C a i l l a t \0 T C * */ // First, look for an ID3v2 tag at the start of the file (offset=0 (fourth parameter of pread()) if (pread(fd, &id3v2_header, sizeof(id3v2_header), 0) != sizeof(id3v2_header)) { // Weird mp3 file < 10 bytes long? loglevel(LOGERROR,"Error: adjust_for_mp3_id_tags: Cannot pread ID3v2 header: %s", strerror(errno)); return; } if (!strncmp(id3v2_header.eyecatcher, ID3V2_VALUE_EYECATCHER, sizeof id3v2_header.eyecatcher) && id3v2_header.version_major <= 254U && id3v2_header.version_minor <= 254U && id3v2_header.size.byte1 <= 127U && id3v2_header.size.byte2 <= 127U && id3v2_header.size.byte3 <= 127U && id3v2_header.size.byte4 <= 127U) { // We have found a valid ID3v2 tag at the start of the media file // Let's skip this tag, but not in the first media file if (file_no > 0) { int tag_size = decode_syncsafe_int(&id3v2_header.size) + 10; // Stored size + size of header if (id3v2_header.flags & ID3V2_FLAG_Footer_present) tag_size += 10; // + size of footer if present mediafile_entry_p->mediafile_offset = tag_size; mediafile_entry_p->size -= tag_size; } } // Look for a 128-byte ID3v1 tag at the end of the file. These start with "TAG". char id3v1_eyecatcher[3]; if (pread(fd, &id3v1_eyecatcher, sizeof(id3v1_eyecatcher), mediafile_entry_p->size - 128) != sizeof(id3v1_eyecatcher)) { // Bug? Cannot read to end of file loglevel(LOGERROR,"Error: adjust_for_mp3_id_tags: Cannot pread ID3v1 header: %s", strerror(errno)); return; } if (!strncmp(id3v1_eyecatcher, ID3V1_VALUE_EYECATCHER, sizeof id3v1_eyecatcher)) { // We have found a 128-byte ID3v1 tag at the end of the media file // Let's skip this tag, but not in the last media file if (file_no < num_files - 1) mediafile_entry_p->size -= 128; } else { // TODO: We could look for an ID3v2 tag at the END of the media file here. // Such a tag is REQUIRED to have a footer present, so we can check for a 10-byte // footer at the end of the file, i.e. look for "3DI" at end-of-file minus 10. // http://www.id3.org/id3v2.4.0-structure } } // ============================================================================================================ static void adjust_for_ogg_metadata_tags(b2b_mediafile_t *mediafile_entry_p, int fd, int num_files, int file_no) { // It seems that OGG files do not produce audio artifacts when comment headers are embedded in the stream, // so we don't care to remove them. } // ============================================================================== static int compose_combinedfile(void) { char fullname[PATH_MAX]; struct stat statbuf; struct dirent **namelist; int num_files, n, fd; off_t offset = 0; // Scan content of the directory : to find all media files for this combined file // Careful: basename is '*' because one file possibly exists and had no extension // Basename - Extension - Dirname - File/Dir mode setup_scandir_selector("*", B2BExt, B2BPath, SCANDIR_FILE_MODE); loglevel(LOGDEBUG,"Debug: compose_combinedfile scandir prepared: pattern:'%s' ext:'%s' dirname:'%s'", scandir_info.scandir_basename_pattern,scandir_info.scandir_file_extensions,scandir_info.scandir_dirname); if ((num_files = scandir(B2BPath, &namelist, (void*)scandir_selector, (void*)my_filelist_compare)) < 0) { loglevel(LOGERROR,"Error: compose_combinedfile: scandir %s: %s", B2BPath, strerror(errno)); return 0; } // Allocate storage for the array of media file entry pointers Cached_CombinedFile.mediafilecount = num_files; if (!(Cached_CombinedFile.mediafiles = malloc(num_files * sizeof(b2b_mediafile_t*)))) { loglevel(LOGERROR,"Error: compose_combinedfile: malloc mediafiles: %s", strerror(errno)); return 0; } memset(Cached_CombinedFile.mediafiles, 0, num_files * sizeof(b2b_mediafile_t*)); // Loop all media files for this combined file, storing info about each in the media file entry for (n = 0; n < num_files; n++) { const char *thisname = namelist[n]->d_name; // Get info about the media file snprintf(fullname, sizeof fullname, "%s/%s", B2BPath, thisname); if (lstat(fullname, &statbuf) < 0) { loglevel(LOGERROR,"Error: compose_combinedfile: Couldn't stat %s", fullname); free_Cached_CombinedFile(); return 0; } // Allocate storage for the media file entry, fill the media file entry b2b_mediafile_t * this_mediafile_entry_p; if (!(this_mediafile_entry_p = malloc(sizeof(b2b_mediafile_t)))) { loglevel(LOGERROR,"Error: compose_combinedfile: malloc mediafile: %s", strerror(errno)); free_Cached_CombinedFile(); return 0; } Cached_CombinedFile.mediafiles[n] = this_mediafile_entry_p; this_mediafile_entry_p->offset = offset; // current offset in the combined file this_mediafile_entry_p->size = statbuf.st_size; this_mediafile_entry_p->mediafile_offset = 0; // Assume no ID tag at start of file // Allocate storage for the media file name and store the media file name if (!(this_mediafile_entry_p->filename = malloc(strlen(thisname) + 1))) { loglevel(LOGERROR,"Error: compose_combinedfile: malloc filename: %s", strerror(errno)); free_Cached_CombinedFile(); return 0; } strcpy(this_mediafile_entry_p->filename, thisname); // Check for MP3 ID tag or OGG Metadata at the start or end of the media file. // If tag/metadata found, adjust size and offsets so that these non-media portions // of the physical media file will not be included in the logical B2B file. if ((fd = open(fullname, O_RDONLY)) < 0) { loglevel(LOGERROR,"Error: compose_combinedfile: Cannot open real media file '%s' for mp3 tag/metadata inspection: %s", fullname, strerror(errno)); free_Cached_CombinedFile(); return 0; } if (!strcasecmp(B2BExt, "mp3")) adjust_for_mp3_id_tags(this_mediafile_entry_p, fd, num_files, n); else if (!strcasecmp(B2BExt, "ogg")) adjust_for_ogg_metadata_tags(this_mediafile_entry_p, fd, num_files, n); close(fd); // loglevel(LOGDEBUG,"Debug: Mediafile id:#%2d size:%ld os.size:%ld mediafile_offset:%lld - CombinedOffset:%lld - Name[%s]", // n, this_mediafile_entry_p->size, statbuf.st_size, this_mediafile_entry_p->mediafile_offset, offset, thisname); loglevel(LOGDEBUG,"Debug: Mediafile id:#%2d size[%10lld] os.size[%10lld] ID3.offset[%5lld] -CombinedOffset[%10lld] Name[%s]", n, this_mediafile_entry_p->size, statbuf.st_size, this_mediafile_entry_p->mediafile_offset, offset, this_mediafile_entry_p->filename); // Media_offset is internal of the media mp3 to skip beginning ID3 information // Size is removing the beginning and ending ID3 size information // Offset is the pointer in final combined file // Increment the running offset to point to the start of the next media file offset += this_mediafile_entry_p->size; free(namelist[n]); } free(namelist); Cached_CombinedFile.size = offset; loglevel(LOGINFO,"Info: Combined (%d) file in size:%lld ",n, offset); return 1; } // *********************************************************************** static int find_mediafile_index(int offset) { int binarySearch(int offset, int low, int high) { if (high < low) return -1; int mid = (low + high) / 2; b2b_mediafile_t* mediafile_p = Cached_CombinedFile.mediafiles[mid]; if (offset >= mediafile_p->offset + mediafile_p->size) return binarySearch(offset, mid + 1, high); else if (offset < mediafile_p->offset) return binarySearch(offset, low, mid - 1); else return mid; } off_t active_offset; if (active_mediafile.mediafile_p && (active_offset = active_mediafile.mediafile_p->offset) != -1 && offset >= active_offset && offset < active_offset + active_mediafile.mediafile_p->size) return active_mediafile.index; else return binarySearch(offset, 0, Cached_CombinedFile.mediafilecount - 1); } void deactivate_mediafile(void) { active_mediafile.mediafile_p = NULL; active_mediafile.index = -1; if (active_mediafile.fd != -1) close(active_mediafile.fd); active_mediafile.fd = -1; } int activate_mediafile(int mediafile_index) { char fullname[PATH_MAX]; b2b_mediafile_t* new_mediafile = Cached_CombinedFile.mediafiles[mediafile_index]; snprintf(fullname, sizeof fullname, "%s/%s", B2BPath, new_mediafile->filename); loglevel(LOGDEBUG,"Debug: Opening B2B media file %s", fullname); // De-activate current media file, if any deactivate_mediafile(); // Activate new media file if ((active_mediafile.fd = open(fullname, O_RDONLY)) < 0) { loglevel(LOGERROR,"Error: CachedB2bSendChunk: Cannot open real media file '%s': %s", fullname, strerror(errno)); return -1; } active_mediafile.mediafile_p = new_mediafile; active_mediafile.index = mediafile_index; return 0; } /******* Public API functions *******/ void CachedB2bClose(void) { if (*Cached_B2BFileName) { loglevel(LOGDEBUG,"Debug: Closing B2B file %s", Cached_B2BFileName); if (active_mediafile.fd != -1) close(active_mediafile.fd); free_Cached_CombinedFile(); } deactivate_mediafile(); *Cached_B2BFileName = *B2BPath = *B2BExt = '\0'; } int CachedB2bIsOpen(const char* path) { return (*Cached_B2BFileName && !strcmp(path, Cached_B2BFileName)); } // =========================================================== int CachedB2bOpen(const char* path) { int isSameFile = !strcmp(path, Cached_B2BFileName); if (!*Cached_B2BFileName) memset(&Cached_CombinedFile, 0, sizeof Cached_CombinedFile); else if (!isSameFile) CachedB2bClose(); // Clean up, close all, reset globals if (isSameFile) return 1; // Normal return for same file else { loglevel(LOGINFO,"Info : Opening NEW B2B file %s", path); char* pExt = strrchr(path, '.'); char* pName = strrchr(path, '/'); if (pExt && pName && (pExt > pName)) { strcpy(B2BExt, pExt + 1); memset(B2BPath, 0, sizeof B2BPath); strncpy(B2BPath, path, pName - path); if (compose_combinedfile()) { deactivate_mediafile(); strcpy(Cached_B2BFileName, path); return 1; // Normal return for new file } else loglevel(LOGERROR,"Error: CachedB2bOpen: Could not compose combined file '%s'", path); } else loglevel(LOGERROR,"Error: CachedB2bOpen: Illegal combined filename: '%s'", path); } return 0; // Error return } //=========================================================== u_int64_t CachedB2bGetSize(void) { return Cached_CombinedFile.size; } //================================================================================== ssize_t CachedB2bSendChunk(int socket, off_t range_start_pos, size_t range_length) { off_t requested_offset; if (*Cached_B2BFileName) { // Find which real media file to read from int mediafile_index = find_mediafile_index(range_start_pos); if (mediafile_index < 0 || mediafile_index >= Cached_CombinedFile.mediafilecount) { loglevel(LOGERROR,"Error: CachedB2bSendChunk: Cannot find real media file (found #%d) for offset %lld.", mediafile_index, range_start_pos); return -1; } if (!active_mediafile.mediafile_p || active_mediafile.mediafile_p != Cached_CombinedFile.mediafiles[mediafile_index]) { // We must open a different real mediafile or the initial one if (activate_mediafile(mediafile_index)) return -1; } // Calculate the actual offset in the actual media file that is requested off_t send_offset = range_start_pos - active_mediafile.mediafile_p->offset; // Get offset into playable part of mediafile // size_t send_length = range_length; size_t rest_length = 0; // If send_offset + length requested, is more than the actual full size, reduce the length accordingly if (send_offset + send_length > active_mediafile.mediafile_p->size) { rest_length = send_offset + send_length - active_mediafile.mediafile_p->size; send_length -= rest_length; } send_offset += active_mediafile.mediafile_p->mediafile_offset; // Get offset into physical mediafile requested_offset = send_offset; #if defined(USE_INTERNAL_SENDFILE) || defined(Linux) ssize_t sent, sent_total; #else /* FreeBSD */ soff_t sent, sent_total; #endif // Send the data from the current media file loglevel(LOGDEBUG,"Debug: B2B request: file[%s] offset:%lld length:%d restlen:%d", Cached_CombinedFile.mediafiles[mediafile_index]->filename,send_offset,send_length,rest_length); #ifdef USE_INTERNAL_SENDFILE if ((sent = my_internal_sendfile(socket, active_mediafile.fd, &send_offset, send_length)) < 0) { #else #ifdef Linux if ((sent = sendfile(sock, active_mediafile.fd, &send_offset, send_length)) < 0) { #else /* FreeBSD */ if (sendfile(active_mediafile.fd, sock, send_offset, send_length, NULL, &sent, 0) < 0) { #endif #endif loglevel(LOGDEBUG,"Debug: CachedB2bSendChunk sendfile offset[%lld] length[%d] : %s",send_offset, send_length, strerror(errno)); return -1; } loglevel(LOGPROTO,"Proto: --> [%d bytes at offset %lld] from %s", sent, requested_offset, active_mediafile.mediafile_p->filename); if (sent != send_length) { loglevel(LOGERROR,"Error: CachedB2bSendChunk, sendfile: sent %d bytes, should send %d.", sent, send_length); return sent; } sent_total = sent; if (rest_length > 0) { // We must open the next real mediafile and send the rest of the requested chunk // TODO: We do not handle the theoretical case where a request from the player spans more than two real media files. // Requesting data past the end of the last media file is ok: The caller will send padding bytes. if (++mediafile_index >= Cached_CombinedFile.mediafilecount) return sent_total; if (activate_mediafile(mediafile_index)) return sent_total; send_offset = active_mediafile.mediafile_p->mediafile_offset; requested_offset = send_offset; // Send the data from the now current media file #ifdef USE_INTERNAL_SENDFILE if ((sent = my_internal_sendfile(socket, active_mediafile.fd, &send_offset, rest_length)) < 0) { #else #ifdef Linux if ((sent = sendfile(sock, active_mediafile.fd, &send_offset, rest_length)) < 0) { #else /* FreeBSD */ if (sendfile(active_mediafile.fd, sock, send_offset, rest_length, NULL, &sent, 0) < 0) { #endif #endif loglevel(LOGDEBUG,"Debug: CachedB2bSendChunk sendfile rest offset[%lld] length[%d] : %s",send_offset, send_length, strerror(errno)); return -1; } loglevel(LOGPROTO,"Proto: --> [%d bytes at offset %lld] from %s", sent, requested_offset, active_mediafile.mediafile_p->filename); sent_total += sent; } // Return number of bytes sent return sent_total; } else { loglevel(LOGERROR,"Error: CachedB2bSendChunk was called when no back-to-back combined file was open.%s",""); return -1; } }