blob: ea5a9711e052b65da93b4329dfff3e9b73065828 [file] [log] [blame]
/*
* Copyright (C) 2008, Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied.
* See the License for the specific language governing permissions
* and limitations under the License.
* -------------------------------------------------------------------
*/
#include "pvmediascanner.h"
#include <stdio.h>
#include "pvlogger.h"
#include "pv_id3_parcom.h"
#include "oscl_string_containers.h"
#include "oscl_file_io.h"
#include "oscl_assert.h"
#include "oscl_lock_base.h"
#include "oscl_snprintf.h"
#include "oscl_string_utf8.h"
#include "pvmf_return_codes.h"
#include "pv_mime_string_utils.h"
#include "pv_id3_parcom_constants.h"
#include "oscl_utf8conv.h"
#include "imp3ff.h"
#include "impeg4file.h"
// Ogg Vorbis includes
#include "ivorbiscodec.h"
#include "ivorbisfile.h"
// Sonivox includes
#include <libsonivox/eas.h>
// used for WMA support
#include "media/mediametadataretriever.h"
#include <media/thread_init.h>
#define MAX_BUFF_SIZE 1024
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#undef LOG_TAG
#define LOG_TAG "PVMediaScanner"
#include "utils/Log.h"
#define MAX_STR_LEN 1000
namespace android {
PVMediaScanner::PVMediaScanner() {}
PVMediaScanner::~PVMediaScanner() {}
static PVMFStatus parseMP3(const char *filename, MediaScannerClient& client)
{
PVID3ParCom pvId3Param;
PVFile fileHandle;
Oscl_FileServer iFs;
uint32 duration;
if (iFs.Connect() != 0)
{
LOGE("iFs.Connect failed\n");
return PVMFFailure;
}
oscl_wchar output[MAX_BUFF_SIZE];
oscl_UTF8ToUnicode((const char *)filename, oscl_strlen((const char *)filename), (oscl_wchar *)output, MAX_BUFF_SIZE);
if (0 != fileHandle.Open((oscl_wchar *)output, Oscl_File::MODE_READ | Oscl_File::MODE_BINARY, iFs) )
{
LOGE("Could not open the input file for reading(Test: parse id3).\n");
return PVMFFailure;
}
fileHandle.Seek(0, Oscl_File::SEEKSET);
pvId3Param.ParseID3Tag(&fileHandle);
fileHandle.Close();
iFs.Close();
//Get the frames information from ID3 library
PvmiKvpSharedPtrVector framevector;
pvId3Param.GetID3Frames(framevector);
uint32 num_frames = framevector.size();
for (uint32 i = 0; i < num_frames;i++)
{
const char* key = framevector[i]->key;
bool isUtf8 = false;
bool isIso88591 = false;
// type should follow first semicolon
const char* type = strchr(key, ';');
if (type == NULL) continue;
type++;
char tracknumkeybuf[100];
if (strncmp(key, "track-info/track-number;", 24) == 0) {
// Java expects the track number key to be called "tracknumber", so
// construct a temporary one here.
snprintf(tracknumkeybuf, sizeof(tracknumkeybuf), "tracknumber;%s", type);
key = tracknumkeybuf;
}
const char* value = framevector[i]->value.pChar_value;
// KVP_VALTYPE_UTF8_CHAR check must be first, since KVP_VALTYPE_ISO88591_CHAR
// is a substring of KVP_VALTYPE_UTF8_CHAR.
// Similarly, KVP_VALTYPE_UTF16BE_WCHAR must be checked before KVP_VALTYPE_UTF16_WCHAR
if (oscl_strncmp(type, KVP_VALTYPE_UTF8_CHAR, KVP_VALTYPE_UTF8_CHAR_LEN) == 0) {
isUtf8 = true;
} else if (oscl_strncmp(type, KVP_VALTYPE_ISO88591_CHAR, KVP_VALTYPE_ISO88591_CHAR_LEN) == 0) {
isIso88591 = true;
}
if (isUtf8) {
// validate to make sure it is legal utf8
uint32 valid_chars;
if (oscl_str_is_valid_utf8((const uint8 *)value, valid_chars)) {
// utf8 can be passed through directly
if (!client.handleStringTag(key, value)) goto failure;
} else {
// treat as ISO-8859-1 if UTF-8 fails
isIso88591 = true;
}
}
// treat it as iso-8859-1 and our native encoding detection will try to
// figure out what it is
if (isIso88591) {
// convert ISO-8859-1 to utf8, worse case is 2x inflation
const unsigned char* src = (const unsigned char *)value;
char* temp = (char *)alloca(strlen(value) * 2 + 1);
if (temp) {
char* dest = temp;
unsigned int uch;
while ((uch = *src++) != 0) {
if (uch & 0x80) {
*dest++ = (uch >> 6) | 0xc0;
*dest++ = (uch & 0x3f) | 0x80;
} else *dest++ = uch;
}
*dest = 0;
if (!client.addStringTag(key, temp)) goto failure;
}
}
// not UTF-8 or ISO-8859-1, try wide char formats
if (!isUtf8 && !isIso88591 &&
(oscl_strncmp(type, KVP_VALTYPE_UTF16BE_WCHAR, KVP_VALTYPE_UTF16BE_WCHAR_LEN) == 0 ||
oscl_strncmp(type, KVP_VALTYPE_UTF16_WCHAR, KVP_VALTYPE_UTF16_WCHAR_LEN) == 0)) {
// convert wchar to utf8
// the id3parcom library has already taken care of byteswapping
const oscl_wchar* src = framevector[i]->value.pWChar_value;
int srcLen = oscl_strlen(src);
// worse case is 3 bytes per character, plus zero termination
int destLen = srcLen * 3 + 1;
char* dest = (char *)alloca(destLen);
if (oscl_UnicodeToUTF8(src, oscl_strlen(src), dest, destLen) > 0) {
if (!client.addStringTag(key, dest)) goto failure;
}
} else if (oscl_strncmp(type, KVP_VALTYPE_UINT32, KVP_VALTYPE_UINT32_LEN) == 0) {
char temp[20];
snprintf(temp, sizeof(temp), "%d", (int)framevector[i]->value.uint32_value);
if (!client.addStringTag(key, temp)) goto failure;
} else {
//LOGE("unknown tag type %s for key %s\n", type, key);
}
}
// extract non-ID3 properties below
{
OSCL_wHeapString<OsclMemAllocator> mp3filename(output);
MP3ErrorType err;
IMpeg3File mp3File(mp3filename, err);
if (err != MP3_SUCCESS) {
LOGE("IMpeg3File constructor returned %d for %s\n", err, filename);
return err;
}
err = mp3File.ParseMp3File();
if (err != MP3_SUCCESS) {
LOGE("IMpeg3File::ParseMp3File returned %d for %s\n", err, filename);
return err;
}
char buffer[20];
duration = mp3File.GetDuration();
sprintf(buffer, "%d", duration);
if (!client.addStringTag("duration", buffer)) goto failure;
}
return PVMFSuccess;
failure:
return PVMFFailure;
}
static PVMFStatus reportM4ATags(IMpeg4File *mp4Input, MediaScannerClient& client)
{
OSCL_wHeapString<OsclMemAllocator> valuestring=NULL;
MP4FFParserOriginalCharEnc charType = ORIGINAL_CHAR_TYPE_UNKNOWN;
uint16 iLangCode=0;
uint64 duration;
uint32 timeScale;
uint16 trackNum;
uint16 totalTracks;
uint16 discNum;
uint16 totalDiscs;
uint32 val;
char buffer[MAX_STR_LEN];
// Title
uint32 i = 0;
for (i = 0; i < mp4Input->getNumTitle(); ++i)
{
mp4Input->getTitle(i,valuestring,iLangCode,charType);
if (oscl_UnicodeToUTF8(valuestring.get_cstr(),valuestring.get_size(),
buffer,sizeof(buffer)) > 0)
{
if (!client.addStringTag("title", buffer)) goto failure;
break;
}
}
// Artist
for (i = 0; i < mp4Input->getNumArtist(); ++i)
{
mp4Input->getArtist(i,valuestring,iLangCode,charType);
if (oscl_UnicodeToUTF8(valuestring.get_cstr(),valuestring.get_size(),
buffer,sizeof(buffer)) > 0)
{
if (!client.addStringTag("artist", buffer)) goto failure;
break;
}
}
// Album
for (i = 0; i < mp4Input->getNumAlbum(); ++i)
{
mp4Input->getAlbum(i,valuestring,iLangCode,charType);
if (oscl_UnicodeToUTF8(valuestring.get_cstr(),valuestring.get_size(),
buffer,sizeof(buffer)) > 0)
{
if (!client.addStringTag("album", buffer)) goto failure;
break;
}
}
// Year
val = 0;
for (i = 0; i < mp4Input->getNumYear(); ++i)
{
mp4Input->getYear(i,val);
sprintf(buffer, "%d", val);
if (buffer[0])
{
if (!client.addStringTag("year", buffer)) goto failure;
break;
}
}
// Writer
if (oscl_UnicodeToUTF8(mp4Input->getITunesWriter().get_cstr(),
mp4Input->getITunesWriter().get_size(),buffer,sizeof(buffer)) > 0)
if (!client.addStringTag("writer", buffer)) goto failure;
// Track Data
trackNum = mp4Input->getITunesThisTrackNo();
totalTracks = mp4Input->getITunesTotalTracks();
sprintf(buffer, "%d/%d", trackNum, totalTracks);
if (!client.addStringTag("tracknumber", buffer)) goto failure;
// Disc number
discNum = mp4Input->getITunesThisDiskNo();
totalDiscs = mp4Input->getITunesTotalDisks();
sprintf(buffer, "%d/%d", discNum, totalDiscs);
if (!client.addStringTag("discnumber", buffer)) goto failure;
// Duration
duration = mp4Input->getMovieDuration();
timeScale = mp4Input->getMovieTimescale();
// adjust duration to milliseconds if necessary
if (timeScale != 1000)
duration = (duration * 1000) / timeScale;
sprintf(buffer, "%lld", duration);
if (!client.addStringTag("duration", buffer)) goto failure;
// Genre
buffer[0] = 0;
for(i=0; i<mp4Input->getNumGenre(); i++)
{
mp4Input->getGenre(i,valuestring,iLangCode,charType);
if (oscl_UnicodeToUTF8(valuestring.get_cstr(),valuestring.get_size(), buffer,sizeof(buffer)) > 0)
break;
}
if (buffer[0]) {
if (!client.addStringTag("genre", buffer)) goto failure;
} else {
uint16 id = mp4Input->getITunesGnreID();
if (id > 0) {
sprintf(buffer, "(%d)", id - 1);
if (!client.addStringTag("genre", buffer)) goto failure;
}
}
return PVMFSuccess;
failure:
return PVMFFailure;
}
static PVMFStatus parseMP4(const char *filename, MediaScannerClient& client)
{
PVFile fileHandle;
Oscl_FileServer iFs;
if (iFs.Connect() != 0)
{
LOGE("Connection with the file server for the parse id3 test failed.\n");
return PVMFFailure;
}
oscl_wchar output[MAX_BUFF_SIZE];
oscl_UTF8ToUnicode((const char *)filename, oscl_strlen((const char *)filename), (oscl_wchar *)output, MAX_BUFF_SIZE);
OSCL_wHeapString<OsclMemAllocator> mpegfilename(output);
IMpeg4File *mp4Input = IMpeg4File::readMP4File(mpegfilename, NULL, NULL, 1 /* parsing_mode */, &iFs);
if (mp4Input)
{
// check to see if the file contains video
int32 count = mp4Input->getNumTracks();
uint32* tracks = new uint32[count];
bool hasAudio = false;
bool hasVideo = false;
uint32_t brand = mp4Input->getCompatibiltyMajorBrand();
if (tracks) {
mp4Input->getTrackIDList(tracks, count);
for (int i = 0; i < count; ++i) {
uint32 trackType = mp4Input->getTrackMediaType(tracks[i]);
OSCL_HeapString<OsclMemAllocator> streamtype;
mp4Input->getTrackMIMEType(tracks[i], streamtype);
if (streamtype.get_size()) {
if (strcmp(streamtype.get_cstr(),"FORMATUNKNOWN") != 0) {
if (trackType == MEDIA_TYPE_AUDIO) {
hasAudio = true;
} else if (trackType == MEDIA_TYPE_VISUAL) {
hasVideo = true;
}
} else {
//LOGI("@@@@@@@@ %100s: %s\n", filename, streamtype.get_cstr());
}
}
}
delete[] tracks;
}
if (brand != 0) { // if filetype exists, see whether it is 3gpp or mp4
char mime[5];
mime[0] = ((brand >> 24) & 0x00FF);
mime[1] = ((brand >> 16) & 0x00FF);
mime[2] = ((brand >> 8) & 0x00FF);
mime[3] = ((brand >> 0) & 0x00FF);
mime[4] = '\0';
if (mime[0] == '3' && mime[1] == 'g' && mime[2] == 'p') { // 3gpp
if (hasVideo) {
if (!client.setMimeType("video/3gpp")) return PVMFFailure;
} else if (hasAudio) {
if (!client.setMimeType("audio/3gpp")) return PVMFFailure;
}
} else if (mime[0] == 'm' && mime[1] == 'p' && mime[2] == '4') { // mp4
if (hasVideo) {
if (!client.setMimeType("video/mp4")) return PVMFFailure;
} else if (hasAudio) {
if (!client.setMimeType("audio/mp4")) return PVMFFailure;
}
} else {
brand = 0;
}
}
if (brand == 0) { // otherwise, mark it as mp4 as previously
if (hasVideo) {
if (!client.setMimeType("video/mp4")) return PVMFFailure;
} else if (hasAudio) {
if (!client.setMimeType("audio/mp4")) return PVMFFailure;
} else {
iFs.Close();
IMpeg4File::DestroyMP4FileObject(mp4Input);
return PVMFFailure;
}
}
PVMFStatus result = reportM4ATags(mp4Input, client);
iFs.Close();
IMpeg4File::DestroyMP4FileObject(mp4Input);
return result;
}
return PVMFSuccess;
}
static PVMFStatus parseOgg(const char *filename, MediaScannerClient& client)
{
int duration;
FILE *file = fopen(filename,"r");
if (!file)
return PVMFFailure;
OggVorbis_File vf;
if (ov_open(file, &vf, NULL, 0) < 0) {
return PVMFFailure;
}
char **ptr=ov_comment(&vf,-1)->user_comments;
while(*ptr){
char *val = strstr(*ptr, "=");
if (val) {
int keylen = val++ - *ptr;
char key[keylen + 1];
strncpy(key, *ptr, keylen);
key[keylen] = 0;
if (!client.addStringTag(key, val)) goto failure;
}
++ptr;
}
// Duration
duration = ov_time_total(&vf, -1);
if (duration > 0) {
char buffer[20];
sprintf(buffer, "%d", duration);
if (!client.addStringTag("duration", buffer)) goto failure;
}
ov_clear(&vf); // this also closes the FILE
return PVMFSuccess;
failure:
ov_clear(&vf); // this also closes the FILE
return PVMFFailure;
}
static PVMFStatus parseMidi(const char *filename, MediaScannerClient& client) {
// get the library configuration and do sanity check
const S_EAS_LIB_CONFIG* pLibConfig = EAS_Config();
if ((pLibConfig == NULL) || (LIB_VERSION != pLibConfig->libVersion)) {
LOGE("EAS library/header mismatch\n");
return PVMFFailure;
}
EAS_I32 temp;
// spin up a new EAS engine
EAS_DATA_HANDLE easData = NULL;
EAS_HANDLE easHandle = NULL;
EAS_RESULT result = EAS_Init(&easData);
if (result == EAS_SUCCESS) {
EAS_FILE file;
file.path = filename;
file.fd = 0;
file.offset = 0;
file.length = 0;
result = EAS_OpenFile(easData, &file, &easHandle);
}
if (result == EAS_SUCCESS) {
result = EAS_Prepare(easData, easHandle);
}
if (result == EAS_SUCCESS) {
result = EAS_ParseMetaData(easData, easHandle, &temp);
}
if (easHandle) {
EAS_CloseFile(easData, easHandle);
}
if (easData) {
EAS_Shutdown(easData);
}
if (result != EAS_SUCCESS) {
return PVMFFailure;
}
char buffer[20];
sprintf(buffer, "%ld", temp);
if (!client.addStringTag("duration", buffer)) return PVMFFailure;
return PVMFSuccess;
}
static PVMFStatus parseWMA(const char *filename, MediaScannerClient& client)
{
sp<MediaMetadataRetriever> retriever = new MediaMetadataRetriever();
retriever->setMode( 1 /*MediaMetadataRetriever.MODE_GET_METADATA_ONLY*/);
status_t status = retriever->setDataSource(filename);
if (status != NO_ERROR) {
LOGE("parseWMA setDataSource failed (%d)", status);
retriever->disconnect();
return PVMFFailure;
}
const char* value;
value = retriever->extractMetadata(METADATA_KEY_IS_DRM_CRIPPLED);
if (value && strcmp(value, "true") == 0) {
// we don't support WMDRM currently
// setting this invalid mimetype will make the java side ignore this file
client.setMimeType("audio/x-wma-drm");
}
value = retriever->extractMetadata(METADATA_KEY_CODEC);
if (value && strcmp(value, "Windows Media Audio 10 Professional") == 0) {
// we don't support WM 10 Professional currently
// setting this invalid mimetype will make the java side ignore this file
client.setMimeType("audio/x-wma-10-professional");
}
value = retriever->extractMetadata(METADATA_KEY_ALBUM);
if (value)
client.addStringTag("album", value);
// Look for "author" tag first, if it is not found, try "artist" tag
value = retriever->extractMetadata(METADATA_KEY_AUTHOR);
if (!value) {
value = retriever->extractMetadata(METADATA_KEY_ARTIST);
}
if (value)
client.addStringTag("artist", value);
value = retriever->extractMetadata(METADATA_KEY_COMPOSER);
if (value)
client.addStringTag("composer", value);
value = retriever->extractMetadata(METADATA_KEY_GENRE);
if (value)
client.addStringTag("genre", value);
value = retriever->extractMetadata(METADATA_KEY_TITLE);
if (value)
client.addStringTag("title", value);
value = retriever->extractMetadata(METADATA_KEY_YEAR);
if (value)
client.addStringTag("year", value);
value = retriever->extractMetadata(METADATA_KEY_CD_TRACK_NUMBER);
if (value)
client.addStringTag("tracknumber", value);
retriever->disconnect();
return PVMFSuccess;
}
status_t PVMediaScanner::processFile(const char *path, const char* mimeType, MediaScannerClient& client)
{
status_t result;
InitializeForThread();
client.setLocale(locale());
client.beginFile();
//LOGD("processFile %s mimeType: %s\n", path, mimeType);
const char* extension = strrchr(path, '.');
if (extension && strcasecmp(extension, ".mp3") == 0) {
result = parseMP3(path, client);
} else if (extension &&
(strcasecmp(extension, ".mp4") == 0 || strcasecmp(extension, ".m4a") == 0 ||
strcasecmp(extension, ".3gp") == 0 || strcasecmp(extension, ".3gpp") == 0 ||
strcasecmp(extension, ".3g2") == 0 || strcasecmp(extension, ".3gpp2") == 0 ||
strcasecmp(extension, ".mpeg") == 0)) {
result = parseMP4(path, client);
} else if (extension && strcasecmp(extension, ".ogg") == 0) {
result = parseOgg(path, client);
} else if (extension &&
( strcasecmp(extension, ".mid") == 0 || strcasecmp(extension, ".smf") == 0
|| strcasecmp(extension, ".imy") == 0)) {
result = parseMidi(path, client);
} else if (extension &&
(strcasecmp(extension, ".wma") == 0 || strcasecmp(extension, ".aac") == 0)) {
//TODO: parseWMA needs to be renamed to reflect what it is really doing,
//ie. using OpenCORE frame metadata utility(FMU) to retrieve metadata.
result = parseWMA(path, client);
} else {
result = PVMFFailure;
}
client.endFile();
return result;
}
static char* doExtractAlbumArt(PvmfApicStruct* aApic)
{
char *data = (char*)malloc(aApic->iGraphicDataLen + 4);
if (data) {
long *len = (long*)data;
*len = aApic->iGraphicDataLen;
memcpy(data + 4, aApic->iGraphicData, *len);
}
return data;
}
static char* extractMP3AlbumArt(int fd)
{
PVID3ParCom pvId3Param;
PVFile file;
OsclFileHandle *filehandle;
Oscl_FileServer iFs;
if(iFs.Connect() != 0)
{
LOGE("Connection with the file server for the parse id3 test failed.\n");
return NULL;
}
FILE *f = fdopen(fd, "r");
filehandle = new OsclFileHandle(f);
file.SetFileHandle(filehandle);
if( 0 != file.Open(NULL, Oscl_File::MODE_READ | Oscl_File::MODE_BINARY, iFs) )
{
LOGE("Could not open the input file for reading(Test: parse id3).\n");
return NULL;
}
file.Seek(0, Oscl_File::SEEKSET);
pvId3Param.ParseID3Tag(&file);
file.Close();
iFs.Close();
//Get the frames information from ID3 library
PvmiKvpSharedPtrVector framevector;
pvId3Param.GetID3Frames(framevector);
uint32 num_frames = framevector.size();
for (uint32 i = 0; i < num_frames; i++)
{
const char* key = framevector[i]->key;
// type should follow first semicolon
const char* type = strchr(key, ';') + 1;
if (type == 0) continue;
const char* value = framevector[i]->value.pChar_value;
const unsigned char* src = (const unsigned char *)value;
if (oscl_strncmp(key,KVP_KEY_ALBUMART,oscl_strlen(KVP_KEY_ALBUMART)) == 0)
{
PvmfApicStruct* aApic = (PvmfApicStruct*)framevector[i]->value.key_specific_value;
if (aApic) {
char* result = doExtractAlbumArt(aApic);
if (result)
return result;
}
}
}
return NULL;
}
static char* extractM4AAlbumArt(int fd)
{
PVFile file;
OsclFileHandle *filehandle;
Oscl_FileServer iFs;
char* result = NULL;
if(iFs.Connect() != 0)
{
LOGE("Connection with the file server for the parse id3 test failed.\n");
return NULL;
}
FILE *f = fdopen(fd, "r");
filehandle = new OsclFileHandle(f);
file.SetFileHandle(filehandle);
oscl_wchar output[MAX_BUFF_SIZE];
oscl_UTF8ToUnicode("", 0, (oscl_wchar *)output, MAX_BUFF_SIZE);
OSCL_wHeapString<OsclMemAllocator> mpegfilename(output);
IMpeg4File *mp4Input = IMpeg4File::readMP4File(
mpegfilename, /* name */
NULL, /* plugin access interface factory */
filehandle,
0, /* parsing_mode */
&iFs);
if (!mp4Input)
return NULL;
PvmfApicStruct* aApic = mp4Input->getITunesImageData();
if (aApic) {
result = doExtractAlbumArt(aApic);
}
IMpeg4File::DestroyMP4FileObject(mp4Input);
return result;
}
char* PVMediaScanner::extractAlbumArt(int fd)
{
InitializeForThread();
int32 ident;
lseek(fd, 4, SEEK_SET);
read(fd, &ident, sizeof(ident));
if (ident == 0x70797466) {
// some kind of mpeg 4 stream
lseek(fd, 0, SEEK_SET);
return extractM4AAlbumArt(fd);
} else {
// might be mp3
return extractMP3AlbumArt(fd);
}
}
}; // namespace android