środa, 8 października 2014

Pojedynek z Androidem - odc. 12 zdjęcia i wideo

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
  • 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

<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

  1. tworzymy instancję
  2. wiążemy z nią aparat
  3. ustawiamy źródła audio i wideo
  4. ustawiamy profil nagrywania
  5. ustawiamy ścieżkę do pliku wynikowego (string)
  6. 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: