Zdjęcia i wideo to tematyka, która zawsze jest już wdzięczna sama w sobie. Bo kto nie chciałby zrobić fotki czy nagrać filmu zamiast np. odczytywać dane, coś tam obliczać czy komunikować się z serwisem? Przyglądając się rozwiązaniom Androida dla multimediów wydaje mi się, że co drugie pojęcie (albo i częściej) gdzieś już widziałem, była gdzieś analogiczna koncepcja, coś nazywa się podobnie czy nawet tak samo… Są też owszem i pewne przekombinowania, ale całość jest dość podobna do ekosystemu Windows, do którego już tradycyjnie będę czynił porównania.
Zacznijmy od podejść do robienia zdjęć i nagrywania wideo. Podejścia z wywołaniem predefiniowanej aplikacji jak i bardziej zaawansowane korzystanie z API połączone z budową własnego interfejsu jest znane dobrze z Windows/Windows Phone. Android w przypadku pierwszej opcji jest bardziej elastyczny idąc - mówiąc językiem Windows - w kontrakty dla aplikacji robiących zdjęcia i nagrywających filmy (w Windows/Windows Phone mamy jedną systemową aplikację, której elementy mogą być wywoływane przez inne aplikacje). Czemu Android zakłada że jak już coś trzymać to koniecznie w External Storage? W Windows Phone wyklarowało się bardziej uniwersalne rozwiązania zakładające różne miejsca przechowywania systemowych multimediów - w pamięci telefonu lub na karcie SD, co wynikło z udostępnienia kart SD dopiero w WP 8. Poza tym często najbardziej topowe modele smartfonów z WP często nie obsługują kart SD, ponieważ oferują dużą pojemność wewnętrznej pamięci. Możliwość wiązania plików w multimedialnym folderze z konkretną aplikacją wydaje się w Androidzie interesujące.
Przejdźmy do robienia zdjęć w wariancie używającym predefiniowanej aplikacji. Wykonanie fotki z opcją podającą ścieżkę dla zwracanego pliku wymaga odrobiny kodu, przykład z nazwą pliku wyliczaną według daty i czasu wydaje się być tym, co nawet zostało wbudowane w sam Windows Phone (automatyczne nadawane przez system nazwy zdjęć są podobne). To, że należy rozgłosić że folder z nowo powstałym zdjęciem jest gotowy (sendBroadcast z Intent.ACTION_MEDIA_MOUNTED) by odświeżyła się galeria, przypomniała mi pewną przypadłość, która czasem zdarzała się w WP.
Proste nagrywanie wideo w Androidzie jest tak proste jak proste zrobienie zdjęcia, choć jest pewna niesymetryczność w API do odbierania wyników.
Bezpośredni dostęp do aparatu - obsługa kamer przedniej i tylnej, podstawowych parametrów wydaje się dość prosta. Unikanie korzystania z kamery, gdy tego nie potrzebujemy czy podczas, gdy aplikacja traci foreground jest także znana z Windows. W sumie nie ma nic dziwnego, sprzęt to sprzęt i rządzi się swoimi prawami. Wyświetlanie podglądu z kamery nie jest może najtrudniejsze, ale trzeba wiedzieć to i owo, nadpisać klasę, przeładować kilka metod. Dobrze jest też zadbać o dobrą orientację pokazywanego obrazu (czemu sami wykonujemy obliczenia?). Obsługa licznych parametrów świadczy o sporych możliwościach, trochę może przekombinowane zapisywanie parametru w zbiorze parametrów, a potem całego zbioru. O metadanych (np. EXIF, geotagi) gdzieś już wcześniej czytałem w czasach poznawania Windows więc jakoś nie wydaje się to zaskakujące.
Nagrywanie wideo klasą MediaRecorder nie jest może najgorsze, ale wymaga wiedzy, co w jakiej kolejności trzeba zrobić, możemy ustawić parametry nieobsługiwane przez nas sprzęt, pewne błędy mogą wyjść dopiero w momencie włączenia nagrywania już po skonfigurowaniu wszystkiego. Jak chcę po przerwie wznowić nagrywanie, to muszę wszystko poresetować i na nowo poinicjować. Nie jest to chyba najlepiej zaprojektowane API, ale może się lepiej nie dało… Wykonywanie zdjęć podczas nagrywania - gdzieś już jakby słyszałem…
MediaStore - pierwsze skojarzenie to libraries w Windows/Windows Phone, w Androidzie to jakby baza zawierające informacje o plikach multimedialnych czy ich miniatury… O ile w Windows nikt nie rozbiera libraries na czynniki pierwsze (choć też to są jakieś zbiory danych), tutaj mamy pewne elementy z anatomii, możemy wymusić wymusić odświeżenie informacji lub jawnie zlecić zapisanie informacji o jakimś konkretnym pliku czy plikach…
Mam też dygresję do obsługi miniatur. W Android jest lepiej niż w Windows Phone, ale gorzej niż w Windows. Wszędzie mamy metodę o nazwie GetThumbnail (z dokładnością do async). W Windows bez żadnych dodatkowych bytów, jakiś id-ków po prostu mogę sobie pobrać miniaturę dla danego pliku i w jakim chcę rozmiarze. W Windows Phone to API też już teraz mamy, ale… jeszcze nie działa. W Android za pomocą Media Store dla pliku o danym id wyciągnę sobie miniaturę w jednym z dwóch predefiniowanych rozmiarów. Jeśli dysponuję tylko nazwą pliku, tu muszę najpierw wykonać niskopoziomowe przeszukanie bazy danych w MediaStore. Mając kursor z zapytania odczytujemy id z pierwszego rekordu, a następnie dopiero pobieramy miniaturę. Czy naprawdę nie dało się prościej tego zaprojektować? Czy są też obsługiwane miniatury do plików wideo ?
Podsumowując, spodziewałem się większej złożoności tematu, ale całość okazała się stosunkowo przystępna, co nie oznacza że zawsze super wygodna. Autor filmów może nie poruszył wszystkiego, bo wiem, że w Windows/Windows Phone znajdzie się jeszcze całkiem sporo ciekawych funkcjonalności związanej z multimediami, jak choćby przykładowo konwersja plików na inne formaty, robienie sekwencji zdjęć (także zaawansowane z rozstrzałem parametrów w WP 8.1), API do edycji wideo w WP 8.1 czy całkiem spore możliwości integracji z systemem WP.
Intro
Dwa podejścia do przechwytywania zdjęć i wideo
- pełna kontrola
- bezpośredni dostęp do aparatu
- nasza aplikacja jest odpowiedzialna za pokazanie podglądu
- nasza aplikacja jest odpowiedzialna za sterowanie zachowaniem aparatu i interakcję z nim
- bardziej skomplikowane oprogramowanie
- proste użycie
- aplikacja deleguje szczegóły obsługi zdjęć i wideo do standardowej aplikacji zdjęć i wideo
- niewielka kontrola
Proste robienie zdjęć
- Wykorzystanie intencji
- intencja z akcją MediaStore.ACTION_IMAGE_CAPTURE
- wynik uzyskiwany za pomocą startActivityForResult
- request code
- response code
- RESULT_OK
- RESULT_CANCELED
- Intent z fotografią
- “data” w extras
- instancja klasy Bitmap
- “data” w extras
- Użytkownik zobaczy aktywność zarejestrowaną do przechwytywania tej akcji
- na większości urządzeń będzie to standardowa aplikacja do zdjęć
- inne aplikacje mogą rejestrować się do przechwytywania tej akcji
- User experience
- wyświetlenie podglądu z aparatu z opcją zamknięcia
- przycisk do zrobienia zdjęcia
- podgląd zdjęcia z opcjami akceptacji, anulowania lub powtórzenia wykonania
public void onMenuItem1Click(MenuItem item) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, 1000);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) {
Bundle extras = null;
Bitmap imageBitmap = null;
ImageView imageView = (ImageView) findViewById(R.id.imageView);
if (resultCode == RESULT_CANCELED) {
return;
}
switch(requestCode) {
case 1000:
extras = resultIntent.getExtras();
imageBitmap = (Bitmap) extras.get(“data”);
break;
}
if (imageBitmap != null) {
imageView.setImageBitmap(imageBitmap);
}
}
Określanie miejsca na plik ze zdjęciem
Intent
- ścieżka dla pliku ze zdjęciem jako extra
- MediaStore.EXTRA_OUTPUT
- ścieżka jako instancja Uri
- podanie ścieżki powoduje zrobienie zdjęcia w pełnej jakości
- onActivityResult
- parametr Intent może być zawsze równy null
Miejsce przechowywania zdjęć
- normalnie w miejscu, do którego może mieć dostęp każdy
- normalnie nie ograniczamy dostępu tylko dla naszej aplikacji
- standardowa lokalizacja w Environment.getExternalStoragePublicDirectory
- dla fotografii stała Environment.DIRECTORY_PICTURES
- normalnie aplikacja tworzy specyficzny dla siebie podfolder
- zdjęcia pozostają nawet po odinstalowaniu aplikacji
- API 7 (Android 2.1.x) lub starsze urządzenia wymagają specjalnego przechwytywania
- używamy Environment.getExternalStorageDirectory
- trzeba jawnie dodać do ścieżki podfolder “Pictures”
Bezpieczna praca z external storage
- zewnętrzy storage może zostać usunięty lub być niedostępny (starsze urządzenia nie zapisują po podłączeniu do komputera)
- potrzebujemy ustalić, czy możemy zapisywać
- Environment.getExternalStorageState (Environment.MEDIA_MOUNTED - bezpieczny zapis)
- Zapis do zewnętrznego storage wymaga w manifeście uprawnienia android.permission.WRITE_EXTERNAL_STORAGE
Zdjęcia mogą być przechowywane w powiązaniu z naszą aplikacją
- pliki są publicznie dostępne
- zostaną automatycznie usunięte po odinstalowaniu aplikacji
- używamy Context.getExternalFilesDir (przekazujemy stałą Environment.DIRECTORY_PICTURES)
File getPhotoDirectory() {
File outputDir = null;
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File pictureDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
outputDir = new File(pictureDir, “Xxx”);
if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
…
outputDir = null;
}
}
}
return outputDir;
}
Uri generateTimeStampPhotoFileUri() {
Uri photoFileUri = null;
File outputDir = getPhotoDirectory();
if (outputDir != null) {
String timeStamp = new SimpleDateFormat(“yyyyMMDD_HHmmss”).format(new Date());
String photoFileName = “IMG_” + timeStamp + “.jpg”;
File photoFile = new File(outputDir, photoFileName);
photoFileUri = Uri.fromFile(photoFile);
}
return photoFileUri;
}
public void onMenuItem2Click(MenuItem item) {
_photoFileUri = generateTimeStampPhotoFileUri();
if (_photoFileUri != null) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, _photoFileUri);
startActivityForResult(intent, 1001);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) {
…
switch(requestCode) {
case 1001:
imageBitmap = BitmapFactory.decodeFile(_photoFileUri.getPath());
break;
}
…
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse(“file://” + Environment.getExternalStorageDirectory());
}
Proste nagrywanie wideo
Delegowanie nagrywania wideo do standardowej aplikacji
- Intent z akcją MediaStore.ACTION_VIDEO_CAPTURE (startActivityForResult)
- standardowa aplikacja
- rozpoczęcie / zatrzymanie nagrywania
- po zatrzymaniu akceptacja, anulowanie lub powtórzenie nagrywania
- określenie nazwy pliku w MediaStore.EXTRA_OUTPUT
- do określania folderu należy skorzystać z Environment.DIRECTORY_MOVIES
- określenie jakości za pomocą MediaStore.EXTRA_VIDEO_QUALITY
- 1 - wysoka jakość, duży plik
- 0 - słabsza jakość, mały plik
Odbieranie wyników nagrania
Wywoływana jest metoda onActivityResult
- response code - sukces/porażka
- przy sukcesie Intent zawiera URI pliku wideo
- getData - dostanie nazwy pliku mp4
- nie ma extra “data”
public void onMenuItem3Click(MenuItem item) {
_videoFileUri = generateTimeStampVideoFileUri();
if (_videoFileUri != null) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, _videoFileUri);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
startActivityForResult(intent, 1002);
}
}
Bezpośredni dostęp do aparatu
Należy oznaczyć aplikację, która używa aparatu
- Google Play
- napisze o tym w informacjach o aplikacji
- zapobiegnie pobraniu aplikacji na urządzenia, które nie wspierają
- deklaracja za pomocą elementu uses-feature
- domyślnie Google Play udostępnia aplikację tylko na urządzenia wspierające daną funkcjonalność
- można oznaczyć opcjonalność przez atrybut required=’false’
- android.hardware.camera - aplikacja może używać dowolnego aparatu (zwykle ten z tyłu)
- android.hardware.camera.front - aplikacja potrzebuje aparatu z przodu
- domyślnie Google Play udostępnia aplikację tylko na urządzenia wspierające daną funkcjonalność
<manifest …>
…
<uses-feature android:name=’android.hardware.camera’ />
…
</manifest>
Weryfikacja obecności aparatu
PackageManager
- metoda hasSystemFeature
- PackageManager.FEATURE_CAMERA (zwykle do aparatu z tyłu)
- PackageManager.FEATURE_CAMERA_FRONT
PackageManager pm = context.getPackageManager(); //metoda w aktywności
boolean hasCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
Pozwolenia dla korzystania z aparatu
- android.permission.CAMERA - korzystanie z aparatu
- android.permission.WRITE_EXTERNAL_STORAGE - zapisywanie obrazu do plików
- android.permission.ACCESS_FINE_LOCATION - tagowanie zdjęć namiarami z GPS
Uzyskiwanie dostępu do aparatu
- statyczna metoda Camera.open
- zwraca referencję Camera
- w wywołaniu bez argumentów otwiera domyślny aparat
- otwarcie konkretnego aparatu
- Camera.open(id) - int id (0 .. Camera.getNumberOfCameras() – 1)
- Camera.release - zakończenie pracy z danym aparatem
Sprawdzenie położenia aparatu przed jego uruchomieniem
- statyczna metoda Camera.getCameraInfo
- int id
- Camera.CameraInfo (tworzy instancję)
- pole CameraInfo.facing
- CameraInfo.CAMERA_FACING_FRONT
- CameraInfo.CAMERA_FACING_BACK
int getFacingCameraId(int facing) {
int cameraId = CAMERA_ID_NOT_SET;
int nCameras = Camera.getNumberOfCameras();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int cameraInfoId=0; cameraInfoId < nCameras; cameraInfoId++) {
Camera.getCameraInfo(cameraInfoId, cameraInfo);
if (cameraInfo.facing == facing) {
cameraId = cameraInfoId;
break;
}
}
return cameraId;
}
Zarządzanie aparatem jak zasobem współdzielonym
Należy trzymać referencję do kamery tak krótko jak to możliwe
- tylko jedna aplikacja może mieć otwartą danę kamerę w zadanym czasie
- wyjątek przy próbie otwarcia kamery trzymanej przez inną aplikację
- zawsze zwalniajmy kamerę kiedy użytkownik opuszcza aktywność
- w metodzie Activity.onPause
- ponowne otworzenie kamery w metodzie Activity.onResume
- w metodzie Activity.onSaveInstanceState możemy zapamiętać id kamery
Implementacja podglądu z kamery
Wyświetlanie podglądu z kamery w aplikacji
- Kamera może rysować podgląd bezpośrednio na instancji klasy Surface
- Umieszczamy Surface w UI
Klasa SurfaceView dostarcza widok opakowujący Surface
- pozwala na umieszczenie Surface w hierarchii widoków w UI aplikacji
- Surface zawarty jest w SurfaceHolder, który zarządza szczegółami layoutu
- rozszerzamy SurfaceView by stworzyć podgląd
- łączymy kamerę z Surface
- implementujemy interfejs SurfaceHolder.Callback
- startujemy i zatrzymujemy podgląd w odpowiedzi na zmiany w dostępności surface
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
Camera _camera;
SurfaceHolder _holder;
public CameraPreview(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
public CameraPreview(Context context) {
super(context);
}
public void ConnectCamera(Camera camera, int cameraId) {
_camera = camera;
int previewOrientation = getCameraPreviewOrientation(cameraId);
_camera.setDisplayOrientation(previewOrientation);
_holder = getHolder();
_holder.addCallback(this);
startPreview();
}
public void releaseCamera() {
if (_camera != null) {
stopPreview();
_camera = null;
}
}
void startPreview() throws IOException {
if (_camera != null && _holder.getSurface() != null) {
try {
_camera.setPreviewDisplay(_holder);
_camera.startPreview();
} catch(Exception e) {
}
}
}
void stopPreview() {
if (_camera != null) {
try {
_camera.stopPreview();
} catch(Exception e) {
}
}
}
public void surfaceCreated(SurfaceHolder surfaceHolder) {
startPreview();
}
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
stopPreview();
startPreview();
}
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
stopPreview();
}
int getCameraPreviewOrientation(int cameraId) {
int temp = 0;
int previewOrientation = 0;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int deviceOrientation = getDeviceOrientationDegrees();
switch(cameraInfo.facing) {
case Camera.CameraInfo.CAMERA_FACING_BACK:
temp = cameraInfo.orientation – deviceOrientation + 360;
previewOrientation = temp % 360;
break;
case Camera.CameraInfo.CAMERA_FACING_FRONT:
temp = (cameraInfo.orientation + deviceOrientation) % 360;
previewOrientation = (360 - temp) % 360;
break;
}
return previewOrientation;
}
int getDeviceOrientationDegrees() {
int degrees = 0;
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
int rotation = windowManager.getDefaultDisplay().getRotation();
switch(rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
…
}
return degrees;
}
}
<com.xxx.CameraPreview
android:id=”@+id/cameraPreview”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:layout_weight=”1”
/>
Zarządzanie podglądem z kamery
- Camera wie jak renderować podgląd na Surface
- Camera.setPreviewDisplay - łączy Camerę z Surface holderem
- Camera.startPreview - rozpoczyna renderowanie podglądu z kamery na Surface
- Camera.stopPreview
- odpowiedź na metody SurfaceHolder.Callback
- surfaceCreated: rozpoczęcie pokazywania podglądu
- surfaceDestroyed: zatrzymanie pokazywania podglądu
- surfaceChanged: zatrzymanie i restart podglądu
Ustawianie orientacji dla podglądu
Może być potrzebne obrócenie podglądu kamery by dopasować się do orientacji urządzenia
- kamery często są inaczej zorientowane niż naturalna orientacja urządzenia
- kamery mogą być odpytywane z orientacji z naturalnej pozycji urządzenia
- Camera.getCameraInfo; wartość w Camera.CameraInfo.orientation
- Android 2.2 (API 8) lub nowszy wspiera obrót podglądu
- Camera.setDisplayOrientation
- kamery z przodu mają dodatkowe wyzwanie: podgląd renderuje się jak lustrzane odbicie
Robienie zdjęcia
Robienie zdjęcia odbywa się asynchronicznie
- inicjalizacja przez wywołanie Camera.takePicture
- powraca od razu bez gotowego zdjęcia
- trzeba dostarczyć implementację callbacku
- interfejs Camera.ShutterCallback
- notyfikacja, kiedy fotografia została po raz pierwszy przechwycona przez sensor
- odtwarzanie odpowiedniego dźwięku lub inny rodzaj feedbacku
- interfejs Camera.PictureCallback
- otrzymuje zdjęcie jako tablicę bajtów
Otrzymywanie danych zdjęcia
Robienie zdjęcia i przetwarzanie zachodzą w fazach
- maksymalnie trzy możliwe wersje danych zdjęcia
- wiele urządzeń wspiera tylko podzbiór
- surowe dane obrazka
- nieprzetworzone dane obrazka bezpośrednio z sensora
- większość kamer nie dostarcza
- dane postview
- pierwszy raz w pełni przetworzona wersja danych (bez kompresji)
- większość kamer dostarcza
- dane obrazka JPEG
- zkompresowana, w pełni zformatowana wersja danych
- w większości przypadków tylko jej potrzebujemy
Camera.takePicture ma dwa przeładowania
- najczęściej używana wersja przyjmuje 3 parametry
- Camera.ShutterCallback
- Camera.PictureCallback dla obrazka postview
- Camera.PictureCallback dla obrazka JPEG
- alternatywne przeładowanie akceptuje dodatkowo
- Camera.PictureCallback dla surowego obrazka
- możemy przekazywać null-e dla parametrów, które nas nie interesują
- podgląd automatycznie się zatrzymuje po wywołaniu takePicture
- nie można zrestartować podglądu dopóki nie otrzymamy finalnego obrazka
void takePicture() {
_selectedCamera.takePicture(null, null, new Camera.PictureCallback() {
public void onPictureTaken(byte[] bytes, Camera camera) {
File f = CameraHelper.generateTimeStampPhotoFile(); // z wcześniejszych sampli
try {
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(f));
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
}
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse(“file://” + Environment.getExternalStorageDirectory())));
_selectedCamera.startPreview();
}
});
}
Zapisywanie zdjęcia na karcie SD
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
- android.permission.WRITE_EXTERNAL_STORAGE
- FileOutputStream (dla poprawienia wydajności opakować w BufferedOutputStream)
Kontrolowanie ustawień aparatu
- Camera.Parameters
- najczęściej spotykane zachowania np. flash, autofocus
- sposób robienia zdjęć np. rozdzielczość
- metadane zdjęcia w pliku JPEG np. obrót aparatu, geotagi
- zachowanie sprzętu powiązanego z aparatem np. zoom
- Camera.getParameters
- zwraca instancję Camera.Parameters
- Camera.setParameters
- zawsze startuje z instancją Camera.Parameters zwróconą przez getParameters
- dla większości wartości, setParameters jest wołane tuż przed zrobieniem zdjęcia
- wszystkie wartości najpierw nanosimy na instancję Parameters
- od razu modyfikuje zachowania
Rozdzielczość fotografii
- Każdy aparat wspiera określone rozdzielczości (zwykle wiele)
- Parameters.getSupportedPictureSizes: List<Camera.Size>
- Parameters.getPictureSize - aktualny wybór
- Wybór rozdzielczości za pomocą Parameters.setPictureSize
- szerokość i wysokość jako int
- trzeba Parameters przekazać do Camera.setParameters
_cameraParameters = _selectedCamera.getParameters();
_supportedPictureSizes = _cameraParameters.getSupportedPictureSizes();
_selectedPictureSize = _cameraParameters.getPictureSize();
…
_selectedPictureSize = _supportedPictureSizes.get(sizeIndex);
_cameraParameters.setPictureSize(_selectedPictureSize.width, _selectedPictureSize.height);
_selectedCamera.setParameters(_cameraParameters);
Metadane fotografii
- EXIF (Exchangeable Image File Format)
- Parameters.setRotation
- przechwytywanie zależy od sterowników aparatu
- niektóre sterowniki zapisują obrót do bloku EXIF
- niektóre sterowniki obracają obraz i zapisują obrót 0 w EXIF lub nie tworzą w ogóle EXIF
- Geotagging
- setGpsLatitude/setGpsLongitude/setGpsAltitude
- setGpsTimestamp
- setGpsProcessingMethod (może być dowolny string, standardowe wartości: GPS, WLAN, CELLID, MANUAL)
- removeGpsData - czyści wszystkie informacje o lokalizacji
int rotation = CameraHelper.getDisplayOrientationForCamera(this, _selectedCameraId) // we wcześniejszych samplach
_cameraParameters.setRotation(rotation);
…
_cameraParameters.setGpsAltitude(altitude);
…
_selectedCamera.setParameters(_cameraParameters);
Zoom
- Parameters.isZoomSupported
- wartość zoom jest wartością całkowitą
- Parameters.getZoom/setZoom
- 0 - brak zoom
- Parameters.getMaxZoom - najbliższy możliwy zoom
- w odpowiedzi na żądanie użytkownika modyfikację zoom robimy najszybciej jak to możliwe
- podgląd i zrobione zdjęcie
- podgląd otrzymuje zmieniony zoom jak tylko zostanie on przekazany do aparatu
- niektóre aparaty wspierają płynny zoom (stopniowa zmiana do zadanego poziomu)
- Parameters.isSmoothZoom
- bezpośrednia komunikacja z kamerą
- Camera.startSmoothZoom (ten sam zakres wartości co Parameters.setZoom)
- asynchroniczne wykonywanie, natychmiastowy powrót samej metody
- nie można wykonywać innych operacji związanych z zoom podczas płynnego zoomowania
- implementujemy Camera.onZoomChangeListener by otrzymywać notyfikacje o zmianach zoom i zakończeniu płynnego zoomowania
ZoomControls zoomControls = (ZoomControls) findViewById(R.id.zoomControls);
zoomControls.setOnZoomInClickListener(
new View.OnClickListener() {
public void onClick(View view) {
zoomIn();
}
}
);
void zoomIn() {
if (_currentZoom < _maxZoom) {
_currentZoom++;
_cameraParameters.setZoom(_currentZoom);
_selectedCamera.setParameters(_cameraParameters);
}
}
public class XActivity extends Activity implements Camera.OnZoomChangeListener {
…
void openCamera() {
…
_isSmoothZoomSupported = _cameraParameters.isSmoothZoomSupported();
if (_isSmoothZoomSupported)
_selectedCamera.setZoomChangeListener(this);
}
void zoomTo(int value) {
if (_currentZoom != value) {
//disable zoom buttons
_selectedCamera.startSmoothZoom(value);
}
}
public void onZoomChange(int zoomValue, boolean stopped, Camera camera) {
if (stopped)
//enable zoom buttons
_currentZoom = zoomValue;
}
}
Nagrywanie wideo
MediaRecorder
- obsługa różnych źródeł wejścia
- duża konfigurowalność
- bardzo szczegółowe ustawienia nagrywania
- spora liczba ogólnych profili dla uproszczenia konfiguracji
- nagrania są zapisywane bezpośrednio w storage’u urządzenia
Stany MediaRecorder
- Przejścia pomiędzy różnymi stanami podczas konfiguracji
- muszą odbywać się w określonej kolejności
Initial – [setAudioSource()/setVideoSource()]* –> Initialized – setOutputFormat() / setProfile –> DataSource Configured
- [setOutputFile() / set zachowania (lub setProfile)]* – prepare() –> Prepared – start() –> Recording:
- stop() –> Initial - release() –> Released
Konfiguracja MediaRecorder
- tworzymy instancję
- wiążemy z nią aparat
- ustawiamy źródła audio i wideo
- ustawiamy profil nagrywania
- ustawiamy ścieżkę do pliku wynikowego (string)
- wywołujemy metodę prepare (try catch)
MediaRecorder mediaRecorder = new MediaRecorder();
…
mediaRecorder.setOutputFile(outputFile.toString());
mediaRecorder.prepare();
Wiązanie z aparatem
Używana jest kamera dostarczona przez aplikację
- włączajmy kamerę tylko do robienia zdjęć
- aplikacja musi zwolnić blokadę na kamerze
- Camera.unlock
- przekazujemy kamerę do MediaRecorder
- MediaRecorder.setCamera
- od Android 3.2 (API 13) aplikacja może nadal ją używać do robienia zdjęć
_camera = Camera.Open(camId);
_camera.setPreviewDisplay(surfaceHolder);
_camera.startPreview();
_camera.unlock();
mediaRecorder.setCamera(_camera);
Ustawianie źródeł audio i wideo
- zawsze używamy tych samych wartości przy nagrywaniu wideo
- MediaRecorder.SetVideoSource - przekazujemy MediaRecorder.VideoSource.CAMERA (kamera przekazana przez MediaRecorder.setCamera)
- MediaRecorder.setAudioSource - przekazujemy MediaRecorder.AudioSource.CAMCORDER (mikrofon z kamery)
Ustawianie profilu
- zamiast ustawiania poszczególnych wartości (co może być skomplikowane, ale daje nam bardzo dużą kontrolę)
- klasa CamcorderProfile reprezentuje każdy profil
- zawiera wszystkie ustawienia dla danego profilu
- stałe reprezentujące każdy profil
- QUALITY_1080P
- QUALITY_720P
- QUALITY_480P
- QUALITY_CIF
- QUALITY_QVGA
- QUALITY_QCIF
- używamy metody get do uzyskania instancji profilu dla danej stałej
- MediaRecorder.setProfile
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_720P);
Nagrywanie wideo
Po przygotowaniu MediaRecorder
- MediaRecorder.start (try catch)
- MediaRecorder.stop
- nie można szybko wznowić nagrywania
- trzeba na nowo zainicjalizować MediaRecorder przed dalszym nagrywaniem
Sprzątanie MediaRecorder
- MediaRecorder.reset – przywraca instancję do stanu Initialize
- MediaRecorder.release - zwolnienie wszystkich zasobów do systemu (MediaRecorder nie trzyma już blokady na kamerze)
- Camera.lock - ponowne zablokowanie kamery
- onPause
- zatrzymanie nagrywania
- pełne wyczyszczenie instancji
- MediaRecorder należy sprzątać przed sprzątaniem kamery
mediaRecorder.reset();
mediaRecorder.release();
camera.lock();
…
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(“file://” + Environment.getExternalStorageDirectory())));
//by pokazało się w galerii
Zarządzanie orientacją kamery
Podobnie jak przy zdjęciach musimy przechowywać informację o położeniu kamery w wideo
- te same obliczenia co przy zdjęciach i poglądzie z jedną różnicą
- używamy tych samych obliczeń dla kamer z przodu co z tyłu
- informację o orientacji zapisujemy w MediaRecorder.setOrientationHint
Jeszcze o profilach
Nie wszystkie kamery wspierają wszystkie profile
- Wyjątek przy próbie użycia profilu powyżej rozdzielczości kamery (podczas rozpoczynania nagrywania)
- Można sprawdzić wsparcie kamery dla danego profilu
- CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_xxx)
- Android posiada dwa specjalne profile, które dostosowują się do kamery
- QUALITY_HIGH - najwyższa wspierana rozdzielczość w kamerze
- QUALITY_LOW - najniższa wspierana rozdzielczość
Media Store
Najbardziej multimedialne aplikacje i funkcje nie korzystają bezpośrednio z systemu plików, tylko z Media Store
- aplikacja Gallery, montowanie USB
Media Scanner Service
- skanuje pliki multimedialne systemowe, by dostarczyć informacje o nich do Media Store
- okresowo wykonuje skanowania, by zapewnić aktualność danych
- skanuje zewnętrzny system plików po zamontowaniu nośnika w urządzeniu
- monitorowanie intencji zawierającej akcję Intent.ACTION_MEDIA_MOUNTED
- system automatycznie wysyła taką intencję po podłączeniu medium do urządzenia
- możliwość wymuszenia przez wywołanie sendBroadcast z taką intencją
- aplikacje mogą wyzwalać skanowanie danego pliku
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse(“file://” + Environment.getExternalStorageDirectory())));
Skanowanie pliku
MediaScannerConnection zapewnia łączność do Media Scanner
- statyczna metoda scanFile do skanowania pojedynczych plików
- context
- tablica ścieżek plików do skanowania
- opcjonalnie typy plików mime (inaczej jest wnioskowany z rozszerzenia)
- opcjonalnie interfejs callback do informowania o zakończeniu
- MediaScannerConnection.onScanCompletedListener
- callback dostarcza URI do informacji o pliku w Media Store
- content://media/external/images/media/1234
- Pliki są widoczne w Media Store jak tylko zakończy się skanowanie
- dużo bardziej efektywniejsze niż używanie Intent.ACTION_MEDIA_MOUNTED
public class XActivity extends Activity {
…
void doScanFile(String fileName) {
String[] filesToScan = {fileName};
MediaScannerConnection.scanFile(this, filesToScan, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onscanCompleted(String filePath, Uri uri) {
}
}
);
}
}
Miniatury
Możemy pobierać miniatury obrazków z Media Store
- MediaStore.Images.Thumbnails - metody i stałe
- stałe określają pożądany rozmiar miniatur
- MediaStore.Images.Thumbnails.MINI_KIND - 512 x 512
- MediaStore.Images.Thumbnails.MICRO_KIND - 96 x 96
- dostępne przy użyciu id plików multimedialnych z Media Store
- id uzyskujemy z URI za pomocą ContentUris.parseId
- dostęp do bitmapy miniatury za pomocą metody getThumbnail
- content resolver
- ID obrazka
- stała z rozmiarem
- opcjonalnie dowolne BitmapFactory.Options
thumbnail = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
Pobieranie miniatury przez nazwę pliku
Mając nazwę pliku możemy znaleźć jego id
- Media Store jest standardowym content providerem
- Content providery mogą być odpytywane za pomocą ContentResolver.query
- ContentResolver może uzyskać poprzez Context.getContentResolver
- Parametry przekazywane do zapytania
- URI: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- kolumny (tablica stringów): MediaStore.Images.Media._ID
- where: MediaStore.Images.Media.Data + “ like ?”
- wartość where (tablica stringów): ścieżka do pliku z obrazkiem
- order by: null
- zwrócony kursor zawiera ID pliku z obrazkiem
final String[] QUERY_COLUMNS = { MediaStore.Images.Media._ID };
final String QUERY_ORDER_BY = MediaStore.Images.Media._ID;
final String QUERY_WHERE = MediaStore.Images.Media.DATA + “ like ? “;
File filePath = new File(photoDirectory, fileName);
String[] queryValues = { filePath.toString() };
Cursor imageCursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, QUERY_COLUMNS,
QUERY_WHERE, queryValues, null);
int idColumnIndex = imageCursor.getColumnIndex(MediaStore.Images.Media._ID);
if (imageCursor.moveToFirst()) {
long id = imageCursor.getLong(idColumnIndex);
}
Brak komentarzy:
Prześlij komentarz