저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

안드로이드 프로그래밍을 하다 보니까, 파일로 캐시를 구현해야 하는 경우가 발생했다. 다음은 파일 캐시가 필요한 경우의 예이다.

  • REST API를 호출한 결과를 파일로 기록해 두고, 다음에는 REST API 호출 없이 파일에서 읽어와 빠른 응답 제공
  • 목록에서 사용되는 웹 이미지를 다운받아 파일로 기록해 두고, 이후 동일 이미지를 빠르게 출력

두 가지 경우 모두 로컬에 데이터를 파일 형태로 캐싱하는 기능을 필요로 한다.


구현할 파일 캐시 기능


여기서 구현해 볼 파일 캐시는 다음의 특징을 갖는다.

  • 두 개 이상의 파일 캐시를 제공한다.
  • 캐시에 보관될 최대 파일 크기를 제한할 수 있다.
  • 시스템의 캐시 폴더에 파일을 기록하기 때문에, 공간이 필요할 경우 안드로이드가 자동으로 오래된 캐시 파일을 삭제한다.


클래스 구성


다음 클래스 다이어그램은 파일 캐시와 관련된 구성 요소의 관계를 표현한 것이다.



[파일 캐시 클래스 다이어그램]


주요 구성 요소는 다음과 같다.


구성 요소 

설명 

FileCacheFactory

FileCache를 생성하고 구할 때 사용되는 Factory 클래스.

FileCache

파일 캐시 기능을 정의한 인터페이스.

FileCacheImpl

CacheStorage를 이용한 FileCache 구현체. 클라이언트의 요청을 

CacheStorage

캐시 파일 저장을 처리하는 클래스. 지정한 최대 크기를 넘길 경우 캐시된 파일 중 오래된 파일을 삭제한다.

CacheStorage.Initializer

CacheStorage를 초기화. 기존에 캐싱된 파일 목록을 등록한다. 

ByteProvider 

FileCache에 데이터를 제공할 때 사용된다. 

ByteProvider 인터페이스 및 보조 클래스


FileCache는 데이터를 파일로 저장하기 때문에 바이트로 데이터를 제공해 주어야 한다. 이를 명시적으로 하기 위해 ByteProvider를 정의하였다. ByteProvider 인터페이스는  다음과 같이 정의되어 있다.


public interface ByteProvider {


void writeTo(OutputStream os) throws IOException;


}


ByteProvider의 몇 가지 구현체를 제공하는 보조 클래스를 만들면 좀 더 편리하게 사용할 수 있다. 다음은 구현 예이다.


public abstract class ByteProviderUtil {


public static ByteProvider create(final InputStream is) {

return new ByteProvider() {

@Override

public void writeTo(OutputStream os) throws IOException {

IOUtils.copy(is, os);

}

};

}


public static ByteProvider create(final File file) {

return new ByteProvider() {

@Override

public void writeTo(OutputStream os) throws IOException {

IOUtils.copy(file, os);

}

};

}


public static ByteProvider create(final String str) {

return new ByteProvider() {

@Override

public void writeTo(OutputStream os) throws IOException {

IOUtils.copy(str, os);

}

};

}

}




CacheStorage 클래스의 구현


핵심 클래스는 CacheStorage 이다. 이 클래스가 가장 길고 복잡하므로 부분 부분 나눠서 구현 코드를 살펴보도록 하겠다.


CacheStorage의 초기화 부분

다음은 CacheStorage의 초기화 부분의 코드이다.

// CacheStorage.java

public class CacheStorage {
private static final String TAG = "CacheStorage";

private File cacheDir;
private Map<String, CacheFile> cacheFileMap;

private long maxBytesSize;
private AtomicLong currentBytesSize = new AtomicLong();;

private ReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

public CacheStorage(File cacheDir, long maxBytesSize) {
this.cacheDir = cacheDir;
this.maxBytesSize = maxBytesSize;
this.cacheFileMap = Collections
.synchronizedMap(new LinkedHashMap<String, CacheFile>(1024));

createCacheDirIfNotExists();
initializing();
}

private void createCacheDirIfNotExists() {
if (cacheDir.exists())
return;
cacheDir.mkdirs();
}

private void initializing() {
new Thread(new Initializer()).start();
}

생성 과정에서는 다음의 작업을 수행한다.

  • 필드 초기화 (생성자)
    • cacheDir: 캐시 경로를 보관하는 File
    • maxBytesSize: 캐시 보관소의 최대 크기
    • cacheFileMap: 캐시 파일 정보를 담을 Map
    • 읽기/쓰기에 사용될 Lock 생성. 캐시 읽기와 쓰기가 동시에 진행되는 것을 방지하기 위한 Lock.
    • currentBytesSize: 캐시 디렉토리에 보관된 전체 파일의 크기
  • createCacheDirIfNotExists(): 캐시 디렉토리가 존재하지 않으면 생성한다.
  • initializing(): 별도 쓰레도로 Initializer를 실행한다.
Initializer는 캐시 디렉토리에 보관된 파일 정보를 cacheFileMap에 로딩하는 기능을 제공한다. Initializer는 CacheStorage의 내부 클래스로서 다음과 같다.

// CacheStorage의 내부 클래스

private class Initializer implements Runnable {

@Override
public void run() {
writeLock.lock();
try {
File[] cachedFiles = cacheDir.listFiles();
for (File file : cachedFiles) {
putFileToCacheMap(file);
}
} catch (Exception ex) {
Log.e(TAG, "CacheStorage.Initializer: fail to initialize - "
+ ex.getMessage(), ex);
} finally {
writeLock.unlock();
}
}
}


Initializer는 캐시 디렉토리에 존재하는 파일 목록을 읽어와 각 파일에 대해 putFileToCacheMap()에 전달한다. putFileToCacheMap() 메서드는 파일 정보를 추가한다. 이 메서드에 대한 내용은 뒤에서 다시 설명한다.


캐시 파일 정보를 구하는 get() 메서드


캐시 스토리지로부터 캐시 파일을 구해주는 get() 메서드는 다음과 같이 구현하였다.


// CacheStorage.java


public File get(String filename) {

readLock.lock();

try {

CacheFile cachdFile = cacheFileMap.get(filename);

if (cachdFile == null) {

return null;

}

if (cachdFile.file.exists()) {

moveHitEntryToFirst(filename, cachdFile);

return cachdFile.file;

}

removeCacheFileFromMap(filename, cachdFile);

return null;

} finally {

readLock.unlock();

}

}


private void moveHitEntryToFirst(String filename, CacheFile cachedFile) {

cacheFileMap.remove(filename);

cacheFileMap.put(filename, cachedFile);

}


private void removeCacheFileFromMap(String filename, CacheFile cachedFile) {

currentBytesSize.addAndGet(-cachedFile.size);

cacheFileMap.remove(filename);

}


다음은 get() 메서드의 실행순서를 정리한 것이다.

  • cacheFileMap 으로부터 해당 이름의 CacheFile이 존재하는지 확인한다.
    • 존재하지 않으면 null을 리턴한다.
  • CacheFile의 file이 실제로 존재하는지 확인한다.
    • 존재하면, moveHitEntryToFirst()를 실행한 뒤에 해당 File을 리턴한다.
    • 존재하지 않으면, removeCacheFileFromMap()을 실행해서 메모리에서 정보를 제거하고 null을 리턴한다.
cacheFileMap은 LinkedHashMap을 사용해서 생성했는데, LinkedHashMap은 등록된 순서 정보를 보관하는 Map이다. 이 Map 구현을 사용한 이유는 전체 파일 크기가 제한된 크기를 넘어선 경우, 최근 등록된 파일이 아닌 오래된 파일을 선택해서 삭제하기 위함이다.

get() 메서드는 cacheFileMap에 데이터가 존재할 경우, File을 리턴하기 전에 moveHitEntryToFirst() 메서드를 호출하는데 이 메서드는 최근에 사용된 파일을 제일 앞으로 보내서 용량 초과시 대상이 되지 않도록 만들어준다.

cacheFileMap에는 존재하지만 실제 대상 파일이 존재하지 않을 수도 있다. (그 이유는 생성한 파일이 캐시 디렉토리에 있기 때문이다.) 이 경우 removeCacheFileFromMap()을 호출해서 전체 파일 크기를 줄이고 cacheFileMap에서 제거한다.

스토리지에 파일 추가하는 write() 메서드

다음은 write() 메서드의 구현이다.

// CacheStorage.java

public void write(String filename, ByteProvider provider) throws IOException {
writeLock.lock();
try {
createCacheDirIfNotExists();
File file = createFile(filename);
copyProviderToFile(provider, file);
putToCachMapAndCheckMaxThresold(file);
} finally {
writeLock.unlock();
}
}

private File createFile(String filename) {
return new File(cacheDir, filename);
}

private void copyProviderToFile(ByteProvider provider, File file)
throws FileNotFoundException, IOException {
BufferedOutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(file));
provider.writeTo(os);
} finally {
IOUtils.close(os);
}
}

private void putToCachMapAndCheckMaxThresold(File file) {
putFileToCacheMap(file);
checkMaxThresoldAndDeleteOldestWhenOverflow();
}

private void putFileToCacheMap(File file) {
cacheFileMap.put(file.getName(), new CacheFile(file));
currentBytesSize.addAndGet(file.length());
}

write() 메서드는 다음의 순서에 따라 파일 쓰기를 처리한다.
  • createCacheDirIfNotExists()를 호출해서 캐시 디렉토리가 없으면 생성한다. 앱의 캐시 디렉토리를 삭제할 수 있기 때문에, 디렉토리 존재 여부를 확인해야 한다.
  • createFile()로 캐시 파일 정보를 생성한다.
  • copyProviderToFile()로 ByteProvider가 제공하는 내용을 파일에 쓴다. ByteProvider.writeTo() 메서드는 파라미터로 제공받은 OutputStream에 데이터를 쓴다.
  • putToCacheMapAndCheckMaxThresold()를 실행해서 cacheFileMap에 캐시 파일 정보를 추가하고, 최대 크기를 초과했는지 확인한다.
putToCachMapAndCheckMaxThresold() 메서드는 다음의 두 작업을 실행한다.
  • putFileToCacheMap() 메서드로 cacheFileMap에 캐시 파일 정보를 추가하고 현재 스토리지 크기(currentBytesSize) 값을 증가한다.
  • checkMaxThresoldAndDeleteOldestWhenOverflow() 메서드를 호출해서, 캐시 디렉토리에 보관된 전체 파일의 크기가 지정한 최대 크기를 초과하면 오래된 파일을 삭제해서 전체 파일 크기를 유지한다.

최대 저장 크기 확인 처리 부분

checkMaxThresoldAndDeleteOldestWhenOverflow() 메서드의 구현 코드는 다음과 같다.

// CacheStorage.java

private void checkMaxThresoldAndDeleteOldestWhenOverflow() {
if (isOverflow()) {
List<Entry<String, CacheFile>> deletingCandidates = getDeletingCandidates();
for (Entry<String, CacheFile> entry : deletingCandidates) {
delete(entry.getKey());
}
}
}

private boolean isOverflow() {
if (maxBytesSize <= 0) {
return false;
}
return currentBytesSize.get() > maxBytesSize;
}

private List<Entry<String, CacheFile>> getDeletingCandidates() {
List<Entry<String, CacheFile>> deletingCandidates = 
new ArrayList<Entry<String, CacheFile>>();
long cadidateFileSizes = 0;
for (Entry<String, CacheFile> entry : cacheFileMap.entrySet()) {
deletingCandidates.add(entry);
cadidateFileSizes += entry.getValue().file.length();
if (currentBytesSize.get() - cadidateFileSizes < maxBytesSize) {
break;
}
}
return deletingCandidates;
}

checkMaxThresoldAndDeleteOldestWhenOverflow() 메서드는 isOverflow()를 이용해서 캐시 디렉토리에 보관된 파일들의 전체 크기가 지정한 최대 크기를 초과했는지 검사한다. 초과한 경우 getDeletingCandidates()를 이용해서 삭제 대상을 구한 뒤에, delete()로 삭제 처리를 한다.

getDeletingCandidates() 메서드는 캐시 파일 중 오래된 파일을 삭제 대상에 추가한다. 최대 크기를 초과하지 않을 때 까지 캐시 파일들을 차례대로 삭제 대상에 추가한다.

스토리지에 파일을 이동시키는 move() 메서드

move() 메서드는 특정 파일을 스토리지로 이동시켜 보관한다.

public void move(String filename, File sourceFile) {
writeLock.lock();
try {
createCacheDirIfNotExists();
File file = createFile(filename);
sourceFile.renameTo(file);
putToCachMapAndCheckMaxThresold(file);
} finally {
writeLock.unlock();
}
}

캐시 파일 삭제 위한 delete() 메서드 및 deleteAll() 메서드

delete() 파일은 간단하다. cacheFileMap에서 캐시파일 정보를 읽어온 뒤, removeCacheFileFromMap() 메서드를 이용해서 메모리에서 정보를 삭제하고 그 다음 파일을 삭제한다.

public void delete(String filename) {
writeLock.lock();
try {
CacheFile cacheFile = cacheFileMap.get(filename);
if (cacheFile == null)
return;

removeCacheFileFromMap(filename, cacheFile);
cacheFile.file.delete();
} finally {
writeLock.unlock();
}
}

public void deleteAll() {
writeLock.lock();
try {
List<String> keys = new ArrayList<String>(cacheFileMap.keySet());
for (String key : keys) {
delete(key);
}
} finally {
writeLock.unlock();
}
}

CacheFile 코드

CacheFile 클래스는 CacheStorage 클래스에 정의된 중첩 클래스로서 다음과 같다.

public class CacheStorage {

...

private static class CacheFile {
public File file;
public long size;

public CacheFile(File file) {
super();
this.file = file;
this.size = file.length();
}
}
}

FileCache 및 FileCacheImpl 구현

FileCache 인터페이스

FileCache 인터페이스는 캐시 목적의 기능을 정의한다.

public interface FileCache {

public FileEntry get(String key);

public void put(String key, ByteProvider provider) throws IOException;

public void put(String key, InputStream is) throws IOException;

public void put(String key, File sourceFile, boolean move) throws IOException;

public void remove(String key);

public void clear();
}

FileCacheImpl 클래스의 구현


파일 캐시를 필요로 하는 코드는 FileCache 타입을 사용하는데, 이 타입의 구현 클래스가 FileCacheImpl이다. FileCacheImpl 클래스는 CacheStorage를 생성하고, get/put/remove 메서드는 cacheStorage에 요청을 전달한다.


public class FileCacheImpl implements FileCache {


private CacheStorage cacheStorage;


public FileCacheImpl(File cacheDir, int maxKBSizes) {

long maxBytesSize = maxKBSizes <= 0 ? 0 : maxKBSizes * 1024;

cacheStorage = new CacheStorage(cacheDir, maxBytesSize);

}


@Override

public FileEntry get(String key) {

File file = cacheStorage.get(keyToFilename(key));

if (file == null) {

return null;

}

if (file.exists()) {

return new FileEntry(key, file);

}

return null;

}


@Override

public void put(String key, ByteProvider provider) throws IOException {

cacheStorage.write(keyToFilename(key), provider);

}


@Override

public void put(String key, InputStream is) throws IOException {

put(key, ByteProviderUtil.create(is));

}


@Override

public void put(String key, File sourceFile, boolean move)

throws IOException {

if (move) {

cacheStorage.move(keyToFilename(key), sourceFile);

} else {

put(key, ByteProviderUtil.create(sourceFile));

}

}


@Override

public void remove(String key) {

cacheStorage.delete(keyToFilename(key));

}


private String keyToFilename(String key) {

String filename = key.replace(":", "_");

filename = filename.replace("/", "_s_");

filename = filename.replace("\\", "_bs_");

filename = filename.replace("&", "_bs_");

filename = filename.replace("*", "_start_");

filename = filename.replace("?", "_q_");

filename = filename.replace("|", "_or_");

filename = filename.replace(">", "_gt_");

filename = filename.replace("<", "_lt_");

return filename;

}


@Override

public void clear() {

cacheStorage.deleteAll();

}


}


캐시 키를 그대로 파일 이름으로 사용할 수 없기 때문에, get/put/remove/move 메서드는 keyToFilename()를 이용해서 파일명으로 사용될 수 없는 문자를 알맞게 치환한다.


FileCacheFactory 클래스의 구현


FileCacheFactory 클래스는 FileCache를 생성하고 제공하는 기능을 제공한다. 안드로이드의 캐시 디렉토리를 먼저 구해야 하기 때문에, 초기화(initialize)를 수행해야 getInstance()를 구하도록 제약을 두었다.


public class FileCacheFactory {


private static boolean initialized = false;

private static FileCacheFactory instance = new FileCacheFactory();


public static void initialize(Context context) {

if (!initialized) {

synchronized (instance) {

if (!initialized) {

instance.init(context);

initialized = true;

}

}

}

}


public static FileCacheFactory getInstance() {

if (!initialized) {

throw new IllegalStateException(

"Not initialized. You must call FileCacheFactory.initialize() before getInstance()");

}

return instance;

}


private HashMap<String, FileCache> cacheMap = new HashMap<String, FileCache>();

private File cacheBaseDir;


private FileCacheFactory() {

}


private void init(Context context) {

cacheBaseDir = context.getCacheDir();

}


public FileCache create(String cacheName, int maxKbSizes) {

synchronized (cacheMap) {

FileCache cache = cacheMap.get(cacheName);

if (cache != null) {

throw new FileCacheAleadyExistException(String.format(

"FileCache[%s] Aleady exists", cacheName));

}

File cacheDir = new File(cacheBaseDir, cacheName);

cache = new FileCacheImpl(cacheDir, maxKbSizes);

cacheMap.put(cacheName, cache);

return cache;

}

}


public FileCache get(String cacheName) {

synchronized (cacheMap) {

FileCache cache = cacheMap.get(cacheName);

if (cache == null) {

throw new FileCacheNotFoundException(String.format(

"FileCache[%s] not founds.", cacheName));

}

return cache;

}

}


public boolean has(String cacheName) {

return cacheMap.containsKey(cacheName);

}

}


파일 캐시 사용법


파일 캐시를 사용하는 방법은 간단하다. 먼저 FileCacheFactory.initialize()로 초기화를 하고, create() 메서드로 캐시를 생성한다. 그 다음 캐시를 필요로 하는 곳에서 get() 메서드를 이용해서 FileCache를 구해서 사용하면 된다.


다음은 FileCache의 사용예를 간단하게 정리한 것이다.


public class SomeLoadActivity extends Activity {


private FileCache fileCache;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

FileCacheFactory.initialize(this);

if (! FileCacheFactory.getInstance().has(cacheName)) {

FileCacheFactory.getInstance().create(cacheName, cacheSize);

}

fileCache = FileCacheFactory.getInstance().get(cacheName);

...

}


public void load() {

...

FileEntry fileEntry = fileCache.get(key);

if (fileEntry != null) {

// 캐시에서 읽어온 데이터로 처리

String data = loadDataFromFile(fileEntry.getFile());

processing(data);

return;

}

// 실제 데이터 로딩 (실제로는 웹에서 비동기로 읽어오는 등의 코드)

String data = loadingDataRealSource();

// 캐시에 보관

fileCache.put(key, ByteProviderUtil.create(dataFile));

// 처리

processing(data);

}


기타 클래스: IOUtils, FileEntry


IOUtils 클래스는 입출력 관련 처리를 위한 보조 클래스로서 본 글에서 사용하는 코드는 다음과 같다.


public abstract class IOUtils {


public static String read(InputStream is) throws IOException {

InputStreamReader reader = null;

try {

reader = new InputStreamReader(is);

StringBuilder builder = new StringBuilder();

char[] readDate = new char[1024];

int len = -1;

while ((len = reader.read(readDate)) != -1) {

builder.append(readDate, 0, len);

}

return builder.toString();

} finally {

close(reader);

}

}


public static void copy(InputStream is, OutputStream out)

throws IOException {

byte[] buff = new byte[4096];

int len = -1;

while ((len = is.read(buff)) != -1) {

out.write(buff, 0, len);

}

}


public static void copy(File source, OutputStream os) throws IOException {

BufferedInputStream is = null;

try {

is = new BufferedInputStream(new FileInputStream(source));

IOUtils.copy(is, os);

} finally {

IOUtils.close(is);

}

}


public static void copy(InputStream is, File target) throws IOException {

OutputStream os = null;

try {

os = new BufferedOutputStream(new FileOutputStream(target));

IOUtils.copy(is, os);

} finally {

IOUtils.close(os);

}

}


public static void copy(String str, OutputStream os) throws IOException {

os.write(str.getBytes());

}


public static void close(Closeable stream) {

if (stream != null) {

try {

stream.close();

} catch (IOException e) {

}

}

}


}


다음은 FileEntry 클래스의 소스 코드이다.


public class FileEntry {


    private String key;

    private File file;


    public FileEntry(String key, File file) {

        this.key = key;

        this.file = file;

    }


    public InputStream getInputStream() throws IOException {

        return new BufferedInputStream(new FileInputStream(file));

    }


    public String getKey() {

        return key;

    }


    public File getFile() {

        return file;

    }


}


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. andu 2013.05.15 17:52 신고  댓글주소  수정/삭제  댓글쓰기

    FileEntry 클래스가 없네요...

  2. andu 2013.05.16 14:05 신고  댓글주소  수정/삭제  댓글쓰기

    FileCacheFactory의 initialize() 정의는 initialize(Context context)인 반면,
    사용법에서는 FileCacheFactory.initialize()로 호출을 하고 있는데,
    이것은 어떤 차이가..

    • 최범균 madvirus 2013.05.19 18:13 신고  댓글주소  수정/삭제

      오류네요... 원본 사용 코드를 그대로 넣지 못해서 복사하고 정리했는데, 그 과정에서 잘못 복사되었어요. FileCacheFactory.initialize(this); 로 수정했습니다.

  3. hong 2013.05.20 22:12 신고  댓글주소  수정/삭제  댓글쓰기

    FileCacheAleadyExistException
    해당 예외처리는 어찌해주셧어요 ??

    다이어그램을 찾아보고 소스부를 봐도 예외처리해준부분은 따로 없어서요 ;;

    익셉션 상속받아 해당 이름으로 예외 클래스 만들어줘서 일단 처리는 해놨지만

    예외처리 클래스 코드가 어떤지 알고싶어서요 부탁드리겠습니다.

    • 최범균 madvirus 2013.05.21 21:51 신고  댓글주소  수정/삭제

      익셉션 직접 만들고 try-catch로 처리해 주심 됩니다.
      FileCacheAleadyExistException 익셉션이 발생한다는 건
      이미 해당 캐시가 존재한다는 것이므로,
      catch 부분에 로그 정도를 남겨주시면 어떨까요?
      전 warn 레벨로 남겼어요.

안드로이드의 MediaPlayer가 스트리밍 프로토콜로 RTSP를 지원하지만 RTSP를 사용해서 미디어를 플레이해보면, 스트리밍이 얼마나 불안정한지 알 수 있다. 안드로이드 3 부터는 HLS를 지원하다고는 하지만 실제로 해보면 기기에 따라 지원이 안 되는 경우도 있다. 그리고, 여전히 2.2~2.3 버전의 기기를 사용하는 사람들도 아직은 상당수 존재할 것 같다. 이런 이류로 예전에 구현 작업을 진행할 때 VOD를 플레이하기 위해 RTSP 대신 HTTP PDL(Progressive download) 방식을 사용했지만, 여전히 LIVE 방송 같은 것을 구현하려면 RTSP를 사용해야했다.


필자가 FFmpeg을 사용해가면서 할 만큼 안드로이드만 집중적으로 팔 수 있는 상황이 아니였기에 포기하고 있었는데, 검색을 하던 중 우연히 Vitamio라는 안드로이드 라이브러리를 알게 되었다. Vitamio는 ARM 기반 프로세스를 위한 ffmpeg 모듈과 이를 이용한 미디어 플레이어 기반 코드를 제공하는 안드로이드 라이브러리로서, 이를 사용하면 비교적 쉽게 안드로이드 기기에서 RTMP, HLS 등의 스트리밍을 플레이할 수 있다. 물론, HTTP PDL도 지원한다.


Vitamio는 안드로이드 2.1 이상을 지원하고 ARMv6, VFP, ARMv7, NEON 등을 지원하기 때문에 현재 시중에 나온 대다수의 안드로이드 기기에서 동작한다.


Vitamio 사용을 위한 개발 환경 설정


아주 간단하다. 다음의 순서대로 진행하면 된다.

  1. http://vitamio.org/vitamios/android-3-dot-0?locale=en 에서 3.0 버전을 다운로드 받는다. (3.0 버전 기준)
  2. 압축받은 파일을 풀면 VitamioBundle 폴더와 VitamioDemo 폴더가 생긴다.
  3. 이클립스에서 VitamioBundle을 안드로이드 프로젝트로 임포트한다.
    1. 프로젝트 이름이 InitActivity로 임포트 되는데, VitamioBundle로 바꿔준다. (안 바꿔줘도 상관은 없다.)
  4. 안드로이드 프로젝트를 생성한다. A프로젝트라고 하자.
  5. A 프로젝트 선택 후, [Project 메뉴] -> [Properties] 메뉴 실행
    1. [Android] 항목 -> 레퍼런스 프로젝트에 VitamioBundle를 추가
    2. [Project References] 항목 -> VitamioBundle 추가
  6. A프로젝트의 AndroidManifest.xml 파일에 다음 Activity 설정을 추가한다.
  7. <activity

    android:name="io.vov.vitamio.activity.InitActivity"

    android:launchMode="singleTop"

    android:theme="@android:style/Theme.NoTitleBar"

    android:windowSoftInputMode="stateAlwaysHidden" />

  8. 이제 A프로젝트에서 Vitamio가 제공하는 VideoView, MediaPlayer 등을 이용해서 구현하면 된다.

Vitamio는 안드로이드가 기본으로 제공하는 VideoView, MediaPlayer 등과 (패키지만 다른) 동일한 이름의 클래스를 제공하고 있다. 따라서, 기존에 안드로이드의 미디어 관련 기능을 사용하고 있다면, 아주 작은 코드 수정만으로도 Vitamio의 기능을 사용할 수 있다. (다운로드 배포판에 함께 포함된 VitamioDemo에 사용코드 예제가 포함되어 있으니 참고하기 바란다.)


Vitamio가 제공하는 기능을 사용하려면 최초에 네이티브 라이브러리를 로딩하는 과정을 거쳐야 하는데, 이 과정은 다음의 코드를 사용하여 처리한다.


import io.vov.vitamio.LibsChecker;


public class MainActivity extends Activity {


@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

if (! LibsChecker.checkVitamioLibs(this)) {

return;

}


따라서, 앱이 실행될 때 최초에 한 번 위 코드를 실행해 주어야 Vitamio가 제공하는 기능을 올바르게 사용할 수 있다.


VitamioBundle 프로젝트에 대한 레퍼런스 없애기


VitamioBundle에 대한 의존을 하려면 함께 하는 개발자들의 이클립스 프로젝트 VitamioBundler을 임포트 해 주어야 하는 불편함이 있다. 여럿이 공동으로 작업을 한다면 VitamioBundler을 사용하지 않고 작업할 수 있으면 더 좋을 것이다.


이를 위한 방법은 역시 간단하다. 다음의 순서대로 진행하면 된다. (참고로, 아래의 코드들은 VitamioBundle을 이용해서 개발하고 있는 OPlayer의 소스 코드 http://code.taobao.org/p/oplayer/src/trunk/OPlayer/ 에서 참고한 것이다.)

  1. 안드로이드 프로젝트의 libs 폴더에 다음의 파일을 복사한다.
    1. VitamioBundle/lib/vitamio.jar
    2. VitamioBundle/lib/armeabi 폴더 및 armeabi-v7a 폴더
  2. 안드로이드 프로젝트의 res/raw 폴더에 다음의 파일을 복사한다.
    1. VitamioBundle/res/raw/libarm.so
  3. io.vov.vitamio 패키지를 생성하고 그 곳에 R.java 클래스를 생성한다. [소스 코드는 아래 참고]
  4. [프로젝트패키지].vitamio 패키지에 다음의 두 클래스를 생성한다.
    1. LibsChecker.java [소스 코드는 아래 참고]
    2. InitActivity.java [소스 코드는 아래 참고]
  5. AndroidManifest.xml 파일에 Vitamio의 InitActivity가 아닌 4-2 과정에서 생성한 InitActivity를 이용해서 액티비티 설정을 추가한다.
그 다음에 Vitamio에 포함된 LibsChecker가 아닌 위 과정에서 생성한 LibsChecker를 이용해서 초기화작업을 진행하면 된다.

import [마이패키지].vitamio.LibsChecker;

public class MainActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!LibsChecker.checkVitamioLibs(this)) {
return;
}

앞서 과정에서 소개한 각 코드는 다음과 같다.

- R.java
package io.vov.vitamio;

public class R {
public static final class raw {
public static final int libarm = [마이패키지].R.raw.libarm;
}
}

- InitActivity.java
/*
 * Copyright (C) 2012 YIXIA.COM
 *
 * 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.
 */
package [마이패키지].vitamio;

import io.vov.vitamio.Vitamio;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.WindowManager;

public class InitActivity extends Activity {
public static final String FROM_ME = "fromVitamioInitActivity";
public static final String EXTRA_MSG = "EXTRA_MSG";
public static final String EXTRA_FILE = "EXTRA_FILE";
private ProgressDialog mPD;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

new AsyncTask<Object, Object, Object>() {
@Override
protected void onPreExecute() {
mPD = new ProgressDialog(InitActivity.this);
mPD.setCancelable(false);
mPD.setMessage("Initializing decoders...");
mPD.show();
}

@Override
protected Object doInBackground(Object... params) {

Vitamio.initialize(getApplicationContext());
uiHandler.sendEmptyMessage(0);
return null;
}
}.execute();
}

private Handler uiHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mPD.dismiss();
Intent src = getIntent();
Intent i = new Intent();
i.setClassName(src.getStringExtra("package"),
src.getStringExtra("className"));
i.setData(src.getData());
i.putExtras(src);
i.putExtra(FROM_ME, true);
startActivity(i);

finish();
}
};
}

- LibsChecker.java
package [마이패키지].vitamio;

import android.app.Activity;
import android.content.Intent;
import io.vov.vitamio.Vitamio;

public final class LibsChecker {
public static final String FROM_ME = "fromVitamioInitActivity";

public static final boolean checkVitamioLibs(Activity ctx) {
if ((!Vitamio.isInitialized(ctx))
&& (!ctx.getIntent().getBooleanExtra("fromVitamioInitActivity",
false))) {
Intent i = new Intent();
i.setClassName(ctx.getPackageName(),
"com.scgs.vitamio.InitActivity");
i.putExtras(ctx.getIntent());
i.setData(ctx.getIntent().getData());
i.putExtra("package", ctx.getPackageName());
i.putExtra("className", ctx.getClass().getName());
ctx.startActivity(i);
ctx.finish();
return false;
}
return true;
}
}

미디어 플레이하기


가장 쉬운 방법은 VitamioBundle에 포함된 io.vov.vitamio.widget.VideoView 클래스를 사용하는 것이다. VitamioDemo에 포함된 VideoViewDemo 클래스에 VideoView의 사용 예제가 포함되어 있다.


또한, VideoView 소스 코드를 보면 io.vov.vitamio.MediaPlayer를 어떻게 사용하는지 알 수 있으므로, MediaPlayer를 직접 이용해서 자신에 맞는 플레이어 화면을 구현할 수도 있다.




Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 팔팔청춘 2013.03.21 10:05 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사드립니다!!

    • 최범균 madvirus 2013.03.25 09:55 신고  댓글주소  수정/삭제

      플레이어 기능 구현에 도움이 되셨으면 좋겠네요.

    • 정한욱 2015.10.11 21:54 신고  댓글주소  수정/삭제

      안녕하세요 Wowza 스트리밍 서버를 통해서 안드로이드 Videoview 로 RTSP URL 받아오려고 하는 초보 개발자 입니다. ㅠㅠ 영상을 보내는데 android 에서 Videoview로 받을때 아예 회색으로 다 깨져버리는 현상이 있는데 어떻게 처리해야하는지 정말로 도움 부탁드립니다. ㅠㅠ 꼭 부탁드리고 귀찮더라도 01094960568연락주시면 감사하겠습니다.

  2. 흐아아앜 2013.06.11 19:30 신고  댓글주소  수정/삭제  댓글쓰기

    demo 코드로 미디어플레이어에서 RTMP 재생을 해봤는데 안되네요

    다른방법을 써야하나요?

  3. 고맙습니다 2013.07.14 16:47 신고  댓글주소  수정/삭제  댓글쓰기

    어떻게 구현할지 막막했는데 좋은 정보 감사합니다!!!
    fms에 있는 동영상을 앱으로 스트리밍 재생하는 연습을 해볼건데 이 라이브러리로 가능한거겠죠??

  4. 지젝 2013.12.09 19:22 신고  댓글주소  수정/삭제  댓글쓰기

    잘 돌아가네요 ^^ 감사합니다.

  5. wind~~~ 2013.12.11 13:37 신고  댓글주소  수정/삭제  댓글쓰기

    감사합니다~
    덕분에 보틀넥 하나를 풀었네요...

    더불어...

    RTSP 서버에 붙을 때 vitamio Fatal signal 11 (SIGSEGV) 라는 에러가 발생할 수 있습니다. 이 때 재생이 안되죠.

    source file 중 MediaPlayerDemo_Video.java에서 mMediaPlayer.getMetadata();를 제거하면 데모를 돌릴 수 있습니다.
    참고 하세요....

    • 정한욱 2015.10.11 21:55 신고  댓글주소  수정/삭제

      안녕하세요 Wowza 스트리밍 서버를 통해서 안드로이드 Videoview 로 RTSP URL 받아오려고 하는 초보 개발자 입니다. ㅠㅠ 영상을 보내는데 android 에서 Videoview로 받을때 아예 회색으로 다 깨져버리는 현상이 있는데 어떻게 처리해야하는지 정말로 도움 부탁드립니다. ㅠㅠ 꼭 부탁드리고 귀찮더라도 01094960568연락주시면 감사하겠습니다.

  6. allday 2014.03.04 11:01 신고  댓글주소  수정/삭제  댓글쓰기

    좋은글 잘 보고 갑니다~

  7. 정한욱 2015.10.11 21:54 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 Wowza 스트리밍 서버를 통해서 안드로이드 Videoview 로 RTSP URL 받아오려고 하는 초보 개발자 입니다. ㅠㅠ 영상을 보내는데 android 에서 Videoview로 받을때 아예 회색으로 다 깨져버리는 현상이 있는데 어떻게 처리해야하는지 정말로 도움 부탁드립니다. ㅠㅠ 꼭 부탁드리고 귀찮더라도 01094960568연락주시면 감사하겠습니다.

  8. minsus 2016.01.07 14:26 신고  댓글주소  수정/삭제  댓글쓰기

    이거 유료 라이브러리죠?

  9. kc 2017.02.24 07:29 신고  댓글주소  수정/삭제  댓글쓰기

    글 잘봤습니다. 감사합니다.ㅋ

    저 하나 질문이 있는데요. vitamio Videoview 및 vitamio Mediaplayer를 다중으로 사용해서 여러 영상을 못 띄우나요?