- 구글 ExoPlayer
https://github.com/google/ExoPlayer
https://developer.android.com/guide/topics/media/exoplayer
- 기본 미디어 플레이와 HLS 미디어 플레이 예제
1. 예제코드
- 어플리케이션 build.gradle 설정
implementation 'com.google.android.exoplayer:exoplayer:2.8.4'
- 기본코드
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private static class EventListener extends Player.DefaultEventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
String stateString;
switch (playbackState) {
case Player.STATE_IDLE:
stateString = "ExoPlayer.STATE_IDLE";
break;
case Player.STATE_BUFFERING:
stateString = "ExoPlayer.STATE_BUFFERING";
break;
case Player.STATE_READY:
stateString = "ExoPlayer.STATE_READY";
break;
case Player.STATE_ENDED:
stateString = "ExoPlayer.STATE_ENDED";
break;
default:
stateString = "UNKNOWN_STATE";
break;
}
Log.d(TAG, "changed state to " + stateString + ", playWhenReady: " + playWhenReady);
}
}
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private SimpleExoPlayer exoPlayer;
private long playbackPosition;
private int currentWindow;
private boolean playWhenReady = true;
private EventListener eventListener = new EventListener();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
super.onResume();
initPlayer();
}
@Override
protected void onPause() {
super.onPause();
releasePlayer();
}
@Override
protected void onStop() {
super.onStop();
releasePlayer();
}
private void initPlayer() {
String url = "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8";
PlayerView playerView = findViewById(R.id.playerView);
TrackSelection.Factory adaptiveTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
exoPlayer = ExoPlayerFactory.newSimpleInstance(new DefaultRenderersFactory(this), new DefaultTrackSelector(adaptiveTrackSelectionFactory), new DefaultLoadControl());
exoPlayer.addListener(new EventListener());
MediaSource mediaSource = url.endsWith(".m3u8") ? buildMediaSourceHLS(Uri.parse(url)) : buildMediaSourceVideo(Uri.parse(url));
exoPlayer.prepare(mediaSource, true, false);
exoPlayer.setPlayWhenReady(playWhenReady);
exoPlayer.seekTo(currentWindow, playbackPosition);
playerView.setPlayer(exoPlayer);
}
private void releasePlayer() {
if (exoPlayer != null) {
playbackPosition = exoPlayer.getCurrentPosition();
currentWindow = exoPlayer.getCurrentWindowIndex();
playWhenReady = exoPlayer.getPlayWhenReady();
exoPlayer.removeListener(eventListener);
exoPlayer.release();
exoPlayer = null;
}
}
private MediaSource buildMediaSourceHLS(Uri uri) {
String userAgent = System.getProperty("http.agent");
Log.d(TAG, "UserAgent : " + userAgent);
DataSource.Factory manifestDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HlsMediaSource hlsMediaSource = new HlsMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri);
return new ConcatenatingMediaSource(hlsMediaSource);
}
private MediaSource buildMediaSourceVideo(Uri uri) {
String userAgent = System.getProperty("http.agent");
Log.d(TAG, "UserAgent : " + userAgent);
ExtractorMediaSource videoSource =
new ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory(TextUtils.isEmpty(userAgent) ? "DefaultHttpDataSourceFactory" : userAgent)).
createMediaSource(uri);
return new ConcatenatingMediaSource(videoSource);
}
}
> buildMediaSourceHLS 메소드는 HLS 미디어를 재생하기위한 메소드
> 예제에서는 PlayerView를 통해 ExoPlayer를 재생하는 예제.
> 안드로이드 MediaPlayer과 동일하게 구현 가능.
> 주요 특징은 별도 검색으로 확인해 볼 것.
> 사용된 HSL 미디어 소스
https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8
추가) 오디오와 영상이 분리된 경우
private MediaSource buildMediaSourceAdaptive(Uri video, Uri audio) {
ExtractorMediaSource videoSource =
new ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory("exoplayer-codelab")).
createMediaSource(video);
ExtractorMediaSource audioSource =
new ExtractorMediaSource.Factory(
new DefaultHttpDataSourceFactory("exoplayer-codelab")).
createMediaSource(audio);
MergingMediaSource mergingMediaSource = new MergingMediaSource(videoSource, audioSource);
return new ConcatenatingMediaSource(mergingMediaSource);
}
2. 샘플코드
소스코드
APK
2019년 3월 14일 목요일
VideoView 기본 예제
- VideoView의 기본 예제
- 플레이, 정지, 구간 스킵의 UI 요소들은 MediaController를 적용하는 예제
- 커스텀 UI로 구현할 경우 MediaController의 코드를 분석한 뒤 구현하는 것을 권장.
1. 기본 소스
- 로드 및 재생
String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
VideoView videoView = findViewById(R.id.videoView);
videoView.setVideoPath(path);
videoView.setMediaController(new MediaController(videoView.getContext()));
videoView.start();
> 기본 MediaController를 적용하면 현재포지션과 재생과 구간 스킵은 간편함.
> 커스텀 UI를 구현할 때 MediaController 소스를 참고해서 구현하는 것을 권장함.
- 포지션 이동
VideoView videoView = findViewById(R.id.videoView);
videoView.resume();
if (currentPosition > 0)
videoView.seekTo(currentPosition);
- 일지정지
VideoView videoView = findViewById(R.id.videoView);
if (videoView.canPause()) {
currentPosition = videoView.getCurrentPosition();
videoView.pause();
}
> VideoView는 resume될 때 현재 진행된 포지션을 유지하지 못한다.
> 실시간 스트리밍을 구현하는 경우가 아니면 currentPosition을 알고 있어야 한다.
- 정지
VideoView videoView = findViewById(R.id.videoView);
videoView.stopPlayback();
- Activity 예제
public class MainActivity extends AppCompatActivity {
private int currentPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
VideoView videoView = findViewById(R.id.videoView);
videoView.setVideoPath(path);
videoView.setMediaController(new MediaController(videoView.getContext()));
videoView.start();
}
@Override
protected void onResume() {
super.onResume();
VideoView videoView = findViewById(R.id.videoView);
videoView.resume();
if (currentPosition > 0)
videoView.seekTo(currentPosition);
}
@Override
protected void onPause() {
VideoView videoView = findViewById(R.id.videoView);
if (videoView.canPause()) {
currentPosition = videoView.getCurrentPosition();
videoView.pause();
}
super.onPause();
}
@Override
protected void onDestroy() {
VideoView videoView = findViewById(R.id.videoView);
videoView.stopPlayback();
super.onDestroy();
}
}
> 사용된 MP4 소스 :
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4
2. 샘플코드
소스코드
APK
- 플레이, 정지, 구간 스킵의 UI 요소들은 MediaController를 적용하는 예제
- 커스텀 UI로 구현할 경우 MediaController의 코드를 분석한 뒤 구현하는 것을 권장.
1. 기본 소스
- 로드 및 재생
String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
VideoView videoView = findViewById(R.id.videoView);
videoView.setVideoPath(path);
videoView.setMediaController(new MediaController(videoView.getContext()));
videoView.start();
> 기본 MediaController를 적용하면 현재포지션과 재생과 구간 스킵은 간편함.
> 커스텀 UI를 구현할 때 MediaController 소스를 참고해서 구현하는 것을 권장함.
- 포지션 이동
VideoView videoView = findViewById(R.id.videoView);
videoView.resume();
if (currentPosition > 0)
videoView.seekTo(currentPosition);
- 일지정지
VideoView videoView = findViewById(R.id.videoView);
if (videoView.canPause()) {
currentPosition = videoView.getCurrentPosition();
videoView.pause();
}
> VideoView는 resume될 때 현재 진행된 포지션을 유지하지 못한다.
> 실시간 스트리밍을 구현하는 경우가 아니면 currentPosition을 알고 있어야 한다.
- 정지
VideoView videoView = findViewById(R.id.videoView);
videoView.stopPlayback();
- Activity 예제
public class MainActivity extends AppCompatActivity {
private int currentPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String path = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4";
VideoView videoView = findViewById(R.id.videoView);
videoView.setVideoPath(path);
videoView.setMediaController(new MediaController(videoView.getContext()));
videoView.start();
}
@Override
protected void onResume() {
super.onResume();
VideoView videoView = findViewById(R.id.videoView);
videoView.resume();
if (currentPosition > 0)
videoView.seekTo(currentPosition);
}
@Override
protected void onPause() {
VideoView videoView = findViewById(R.id.videoView);
if (videoView.canPause()) {
currentPosition = videoView.getCurrentPosition();
videoView.pause();
}
super.onPause();
}
@Override
protected void onDestroy() {
VideoView videoView = findViewById(R.id.videoView);
videoView.stopPlayback();
super.onDestroy();
}
}
> 사용된 MP4 소스 :
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_10mb.mp4
2. 샘플코드
소스코드
APK
2019년 3월 13일 수요일
안드로이드 데이터 바인딩 예제 - Android Jetpack
- 레퍼런스
https://developer.android.com/topic/libraries/data-binding?hl=ko
1. 데이터 바인딩
- gradle.properties 설정
android.databinding.enableV2=true
- xml 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="app.example.databindingapp.BuildConfig" />
<variable
name="listAdapter"
type="app.example.databindingapp.ListItemAdapter" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{BuildConfig.APPLICATION_ID}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/packageName"
android:adapter="@{listAdapter}" />
</RelativeLayout>
</layout>
> layout 안에 data와 실제 레이아웃 구성으로 나눠짐
> import와 variable 구분 할 것.
> 자세한 문법은 구글 레퍼런스 쪽에서 확인
> 바인딩을 사용하는 View는 id를 지정할 것.
- Activity
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
> setContentView 대신 DataBindingUtil의 setContentView를 사용한다.
> xml 레이아웃에서 데이터 바인딩을 정의하면 generatedJava 쪽에 코드가 자동 생성된다.
> 생성이 안되는 경우 Rebuild Project로 자동 생성을 시도하면 된다.
2. ListView를 데이터 바인딩으로 구성하기
- row_text_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="item"
type="java.lang.String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{item}"
android:textColor="@android:color/black"
android:textSize="16dp" />
</RelativeLayout>
</layout>
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<import type="app.example.databindingapp.BuildConfig" />
<variable
name="listAdapter"
type="app.example.databindingapp.ListItemAdapter" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{BuildConfig.APPLICATION_ID}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/versionName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:padding="16dp"
android:text="@{BuildConfig.VERSION_NAME}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/mode"
android:layout_below="@id/packageName"
android:adapter="@{listAdapter}" />
<TextView
android:id="@+id/mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/black"
android:gravity="center"
android:padding="16dp"
android:text="DEBUG APP"
android:textColor="@android:color/white"
android:textSize="16dp"
android:visibility="@{BuildConfig.DEBUG ? View.VISIBLE : View.GONE}" />
</RelativeLayout>
</layout>
- MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
list.add("Android");
ListItemAdapter listItemAdapter = new ListItemAdapter(getBaseContext(), list);
activityMainBinding.setListAdapter(listItemAdapter);
}
}
> 데이터 바인딩으로 처리하면 전체 코드는 매우 간결해진다.
> 데이터 바인딩을 적절하게 사용하면 편리하지만 단순히 코드의 양을 줄이기 위한 용도로 사용한다면 부작용이 발생한다.
3. 샘플코드
소스코드
APK
https://developer.android.com/topic/libraries/data-binding?hl=ko
1. 데이터 바인딩
- gradle.properties 설정
android.databinding.enableV2=true
- xml 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="app.example.databindingapp.BuildConfig" />
<variable
name="listAdapter"
type="app.example.databindingapp.ListItemAdapter" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{BuildConfig.APPLICATION_ID}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/packageName"
android:adapter="@{listAdapter}" />
</RelativeLayout>
</layout>
> layout 안에 data와 실제 레이아웃 구성으로 나눠짐
> import와 variable 구분 할 것.
> 자세한 문법은 구글 레퍼런스 쪽에서 확인
> 바인딩을 사용하는 View는 id를 지정할 것.
- Activity
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
> setContentView 대신 DataBindingUtil의 setContentView를 사용한다.
> xml 레이아웃에서 데이터 바인딩을 정의하면 generatedJava 쪽에 코드가 자동 생성된다.
> 생성이 안되는 경우 Rebuild Project로 자동 생성을 시도하면 된다.
2. ListView를 데이터 바인딩으로 구성하기
- row_text_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="item"
type="java.lang.String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{item}"
android:textColor="@android:color/black"
android:textSize="16dp" />
</RelativeLayout>
</layout>
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<import type="app.example.databindingapp.BuildConfig" />
<variable
name="listAdapter"
type="app.example.databindingapp.ListItemAdapter" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@{BuildConfig.APPLICATION_ID}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/versionName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:padding="16dp"
android:text="@{BuildConfig.VERSION_NAME}"
android:textColor="@android:color/black"
android:textSize="16dp" />
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/mode"
android:layout_below="@id/packageName"
android:adapter="@{listAdapter}" />
<TextView
android:id="@+id/mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/black"
android:gravity="center"
android:padding="16dp"
android:text="DEBUG APP"
android:textColor="@android:color/white"
android:textSize="16dp"
android:visibility="@{BuildConfig.DEBUG ? View.VISIBLE : View.GONE}" />
</RelativeLayout>
</layout>
- MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
list.add("Android");
ListItemAdapter listItemAdapter = new ListItemAdapter(getBaseContext(), list);
activityMainBinding.setListAdapter(listItemAdapter);
}
}
> 데이터 바인딩으로 처리하면 전체 코드는 매우 간결해진다.
> 데이터 바인딩을 적절하게 사용하면 편리하지만 단순히 코드의 양을 줄이기 위한 용도로 사용한다면 부작용이 발생한다.
3. 샘플코드
소스코드
APK
라벨:
데이터 바인딩,
안드로이드,
Android Jetpack,
Data Binding
위치:
대한민국
안드로이드 PIP 예제 - Picture In Picture
- PIP 지원
https://developer.android.com/guide/topics/ui/picture-in-picture?hl=ko
https://developer.android.com/about/versions/oreo/android-8.0?hl=ko#opip
1. 기본구성
- AndroidManifest.xml
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
- 미디어 플레이어 코드가 구현된 상태에서 Actvity 추가사항들.
PictureInPictureParams.Builder pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(build);
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
}
> PIP 동작 상태가 변경되면 onPictureInPictureModeChanged 메소드가 호출됩니다.
> enterPictureInPictureMode 메소드를 호출하면 PIP 모드로 동작합니다.
- Activity 전체코드
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private String mediaURL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4";
private MediaPlayer mediaPlayer;
private boolean minimize;
private final Object pictureInPictureParamsBuilder;
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
} else {
pictureInPictureParamsBuilder = null;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
setContentView(R.layout.activity_main);
SurfaceView surfaceView = findViewById(R.id.surfaceView);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
initMediaPlayer(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
View actionPIP = findViewById(R.id.actionPIP);
actionPIP.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE);
actionPIP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
minimize();
}
});
}
private void initMediaPlayer(SurfaceHolder holder) {
holder.getSurface();
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
mediaPlayer.setSurface(holder.getSurface());
try {
mediaPlayer.setDataSource(getBaseContext(), Uri.parse(mediaURL));
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
private void play() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying() == false) {
mediaPlayer.start();
}
}
}
@Override
protected void onResume() {
super.onResume();
play();
}
@Override
protected void onPause() {
super.onPause();
if (minimize == false)
if (mediaPlayer != null) {
mediaPlayer.pause();
}
}
@Override
protected void onDestroy() {
if (mediaPlayer != null) {
mediaPlayer.release();
}
super.onDestroy();
}
/**
* PIP 모드
*/
private void minimize() {
if (mediaPlayer == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (pictureInPictureParamsBuilder instanceof PictureInPictureParams.Builder) {
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = ((PictureInPictureParams.Builder) pictureInPictureParamsBuilder).setAspectRatio(aspectRatio).build();
minimize = true;
enterPictureInPictureMode(build);
}
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
if (isInPictureInPictureMode) {
play();
} else {
minimize = false;
}
}
}
3. 샘플코드
소스코드
APK
샘플 mp4 영상
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4
구글 샘플코드
https://github.com/googlesamples/android-PictureInPicture/#readme
https://developer.android.com/guide/topics/ui/picture-in-picture?hl=ko
https://developer.android.com/about/versions/oreo/android-8.0?hl=ko#opip
1. 기본구성
- AndroidManifest.xml
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
- 미디어 플레이어 코드가 구현된 상태에서 Actvity 추가사항들.
PictureInPictureParams.Builder pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(build);
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
}
> PIP 동작 상태가 변경되면 onPictureInPictureModeChanged 메소드가 호출됩니다.
> enterPictureInPictureMode 메소드를 호출하면 PIP 모드로 동작합니다.
- Activity 전체코드
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private String mediaURL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4";
private MediaPlayer mediaPlayer;
private boolean minimize;
private final Object pictureInPictureParamsBuilder;
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
} else {
pictureInPictureParamsBuilder = null;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
setContentView(R.layout.activity_main);
SurfaceView surfaceView = findViewById(R.id.surfaceView);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
initMediaPlayer(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
View actionPIP = findViewById(R.id.actionPIP);
actionPIP.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE);
actionPIP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
minimize();
}
});
}
private void initMediaPlayer(SurfaceHolder holder) {
holder.getSurface();
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
mediaPlayer.setSurface(holder.getSurface());
try {
mediaPlayer.setDataSource(getBaseContext(), Uri.parse(mediaURL));
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
private void play() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying() == false) {
mediaPlayer.start();
}
}
}
@Override
protected void onResume() {
super.onResume();
play();
}
@Override
protected void onPause() {
super.onPause();
if (minimize == false)
if (mediaPlayer != null) {
mediaPlayer.pause();
}
}
@Override
protected void onDestroy() {
if (mediaPlayer != null) {
mediaPlayer.release();
}
super.onDestroy();
}
/**
* PIP 모드
*/
private void minimize() {
if (mediaPlayer == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (pictureInPictureParamsBuilder instanceof PictureInPictureParams.Builder) {
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = ((PictureInPictureParams.Builder) pictureInPictureParamsBuilder).setAspectRatio(aspectRatio).build();
minimize = true;
enterPictureInPictureMode(build);
}
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
if (isInPictureInPictureMode) {
play();
} else {
minimize = false;
}
}
}
3. 샘플코드
소스코드
APK
샘플 mp4 영상
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4
구글 샘플코드
https://github.com/googlesamples/android-PictureInPicture/#readme
라벨:
안드로이드,
Picture In Picture,
PIP
위치:
대한민국
FCM 클라이언트 예제
1. FCM 환경구성
- 파이어베이스 프로젝트 생성 및 어플리케이션 등록
https://console.firebase.google.com/
> keytool을 이용해서 어플리케이션을 파이어베이스 프로젝트와 연동(인증)
> keytool의 위치는 JAVA SDK의 bin 디렉토리에 있음.
https://developers.google.com/android/guides/client-auth
> google-services.json 파일은 [프로젝트]/app 디렉토리에 포함시킨다.
> 프로젝트 연동을 확인 하면 기본 설정은 완료.
2. 기본구성
- FCM gradle 구성
https://firebase.google.com/docs/cloud-messaging/android/client?hl=ko
- FirebaseMessagingService 상속
public class MyFirebaseMessagingService extends FirebaseMessagingService{}
- FCM 서비스 등록 - AndroidManifest.xml
<service android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
> MyFirebaseMessagingService는 FirebaseMessagingService를 상속 한다.
2. 기본코드
- MyFirebaseMessagingService
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = MyFirebaseMessagingService.class.getSimpleName();
/*
* Activity가 onResume 상태가 아닐 때는 Cloud Message에서 전송한 메시지는 수신되지 않는다.
*/
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
String from = remoteMessage.getFrom();
String messageId = remoteMessage.getMessageId();
String messageType = remoteMessage.getMessageType();
Map<String, String> map = remoteMessage.getData();
int size = map.size();
Log.d(TAG, "RemoteMessage from : " + from);
Log.d(TAG, "RemoteMessage id : " + messageId);
Log.d(TAG, "RemoteMessage type : " + messageType);
Log.d(TAG, "RemoteMessage size : " + size);
if (size > 0) {
JSONObject jsonObject = new JSONObject(map);
Log.d(TAG, "RemoteMessage data : " + jsonObject.toString());
}
RemoteMessage.Notification notification = remoteMessage.getNotification();
if (notification != null) {
String title = notification.getTitle();
String body = notification.getBody();
Log.d(TAG, "RemoteMessage notification title : " + title);
Log.d(TAG, "RemoteMessage notification body : " + body);
}
}
@Override
public void onNewToken(String s) {
super.onNewToken(s);
Log.d(TAG, "TOKEN : " + s);
}
}
> 파이어베이스의 Cloud Message는 액티비티 상태에 따라 메시지 수신 결과가 달라진다.
> FCM 메시지의 data 영역만 사용할 때는 항상 MyFirebaseMessagingService 가 수신한다.
- MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Task<InstanceIdResult> id = FirebaseInstanceId.getInstance().getInstanceId();
id.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
// 토큰 확인
if (task.isSuccessful()) {
InstanceIdResult result = task.getResult();
String id = result.getId();
String token = result.getToken();
Log.d(TAG, "Token id : " + id);
Log.d(TAG, "Token : " + token);
TextView tokenView = findViewById(R.id.token);
tokenView.setText(task.getResult().getToken());
} else {
Log.d(TAG, "Token Exception : " + task.getException().toString());
}
}
});
}
}
- AndroidManifest.xml 코드
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.example.fcmapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
- Cloud Message 전송 테스트
> 백그라운드 또는 알림센터에서 메시지가 수신된다.
3. 샘플코드
소스코드
APK
- 파이어베이스 프로젝트 생성 및 어플리케이션 등록
https://console.firebase.google.com/
> keytool을 이용해서 어플리케이션을 파이어베이스 프로젝트와 연동(인증)
> keytool의 위치는 JAVA SDK의 bin 디렉토리에 있음.
https://developers.google.com/android/guides/client-auth
> google-services.json 파일은 [프로젝트]/app 디렉토리에 포함시킨다.
> 프로젝트 연동을 확인 하면 기본 설정은 완료.
2. 기본구성
- FCM gradle 구성
https://firebase.google.com/docs/cloud-messaging/android/client?hl=ko
- FirebaseMessagingService 상속
public class MyFirebaseMessagingService extends FirebaseMessagingService{}
- FCM 서비스 등록 - AndroidManifest.xml
<service android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
> MyFirebaseMessagingService는 FirebaseMessagingService를 상속 한다.
2. 기본코드
- MyFirebaseMessagingService
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = MyFirebaseMessagingService.class.getSimpleName();
/*
* Activity가 onResume 상태가 아닐 때는 Cloud Message에서 전송한 메시지는 수신되지 않는다.
*/
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
String from = remoteMessage.getFrom();
String messageId = remoteMessage.getMessageId();
String messageType = remoteMessage.getMessageType();
Map<String, String> map = remoteMessage.getData();
int size = map.size();
Log.d(TAG, "RemoteMessage from : " + from);
Log.d(TAG, "RemoteMessage id : " + messageId);
Log.d(TAG, "RemoteMessage type : " + messageType);
Log.d(TAG, "RemoteMessage size : " + size);
if (size > 0) {
JSONObject jsonObject = new JSONObject(map);
Log.d(TAG, "RemoteMessage data : " + jsonObject.toString());
}
RemoteMessage.Notification notification = remoteMessage.getNotification();
if (notification != null) {
String title = notification.getTitle();
String body = notification.getBody();
Log.d(TAG, "RemoteMessage notification title : " + title);
Log.d(TAG, "RemoteMessage notification body : " + body);
}
}
@Override
public void onNewToken(String s) {
super.onNewToken(s);
Log.d(TAG, "TOKEN : " + s);
}
}
> 파이어베이스의 Cloud Message는 액티비티 상태에 따라 메시지 수신 결과가 달라진다.
> FCM 메시지의 data 영역만 사용할 때는 항상 MyFirebaseMessagingService 가 수신한다.
- MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Task<InstanceIdResult> id = FirebaseInstanceId.getInstance().getInstanceId();
id.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
// 토큰 확인
if (task.isSuccessful()) {
InstanceIdResult result = task.getResult();
String id = result.getId();
String token = result.getToken();
Log.d(TAG, "Token id : " + id);
Log.d(TAG, "Token : " + token);
TextView tokenView = findViewById(R.id.token);
tokenView.setText(task.getResult().getToken());
} else {
Log.d(TAG, "Token Exception : " + task.getException().toString());
}
}
});
}
}
- AndroidManifest.xml 코드
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.example.fcmapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
- Cloud Message 전송 테스트
> 백그라운드 또는 알림센터에서 메시지가 수신된다.
3. 샘플코드
소스코드
APK
2019년 3월 12일 화요일
FCM 전송 예제
- GCM은 사용하지 말것.
https://developers.google.com/cloud-messaging/
- FCM 레퍼런스 :
https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko
- 예제소스는 HTTP v1 샘플소스
- 메시지 포멧은 JSON 이므로 JSON 라이브러리를 사용을 권장.
1. 메시지 전송
- 기본코드
String urlStr = "https://fcm.googleapis.com/fcm/send";
String authorization = "key=[클라우드 메시지 서버 키]";
String token = "[클라이언트 FCM 토큰]";
String data = "\"data\": {\r\n" + " \"en\": \"abc\",\r\n" + " \"ko\": \"가나다\"\r\n" + " }";
String notification = "\"notification\" : {\r\n"
+ " \"body\" : \"This is an FCM notification message!\",\r\n"
+ " \"title\" : \"FCM Message\"\r\n" + " }";
URL url = null;
HttpURLConnection connection = null;
BufferedOutputStream bos = null;
BufferedReader reader = null;
try {
url = new URL(urlStr);
connection = urlStr.startsWith("https://") ? (HttpsURLConnection) url.openConnection()
: (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
connection.setRequestProperty("cache-control", "no-cache");
connection.setRequestProperty("Authorization", authorization);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.connect();
bos = new BufferedOutputStream(connection.getOutputStream());
String message = "{\"to\" : \"" + token + "\"," + data + "," + notification + "}";
bos.write(message.getBytes("UTF-8"));
bos.flush();
bos.close();
int responseCode = connection.getResponseCode();
String responseMessage = connection.getResponseMessage();
StringBuffer buffer = null;
if (responseCode == HttpURLConnection.HTTP_OK) {
buffer = new StringBuffer();
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String temp = null;
while ((temp = reader.readLine()) != null) {
buffer.append(temp);
}
reader.close();
}
connection.disconnect();
System.out.println(String.format("Response : %d, %s", responseCode, responseMessage));
System.out.println("Response DATA : ");
System.out.println(buffer == null ? "NULL " : buffer.toString());
} catch (IOException e) {
e.printStackTrace();
}
> POST 전송 방식
> data만 사용하면 항상 백그라운드 서비스가 메시지를 수신
> notification을 적용할 경우 Activity가 Foreground 상태일 때와 Background 상태일 때 응답 방식이 변경됨.
> 한글깨짐 문제가 될 수 있으므로 UTF-8 인코딩을 적용.
https://developers.google.com/cloud-messaging/
- FCM 레퍼런스 :
https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko
- 예제소스는 HTTP v1 샘플소스
- 메시지 포멧은 JSON 이므로 JSON 라이브러리를 사용을 권장.
1. 메시지 전송
- 기본코드
String urlStr = "https://fcm.googleapis.com/fcm/send";
String authorization = "key=[클라우드 메시지 서버 키]";
String token = "[클라이언트 FCM 토큰]";
String data = "\"data\": {\r\n" + " \"en\": \"abc\",\r\n" + " \"ko\": \"가나다\"\r\n" + " }";
String notification = "\"notification\" : {\r\n"
+ " \"body\" : \"This is an FCM notification message!\",\r\n"
+ " \"title\" : \"FCM Message\"\r\n" + " }";
URL url = null;
HttpURLConnection connection = null;
BufferedOutputStream bos = null;
BufferedReader reader = null;
try {
url = new URL(urlStr);
connection = urlStr.startsWith("https://") ? (HttpsURLConnection) url.openConnection()
: (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
connection.setRequestProperty("cache-control", "no-cache");
connection.setRequestProperty("Authorization", authorization);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.connect();
bos = new BufferedOutputStream(connection.getOutputStream());
String message = "{\"to\" : \"" + token + "\"," + data + "," + notification + "}";
bos.write(message.getBytes("UTF-8"));
bos.flush();
bos.close();
int responseCode = connection.getResponseCode();
String responseMessage = connection.getResponseMessage();
StringBuffer buffer = null;
if (responseCode == HttpURLConnection.HTTP_OK) {
buffer = new StringBuffer();
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String temp = null;
while ((temp = reader.readLine()) != null) {
buffer.append(temp);
}
reader.close();
}
connection.disconnect();
System.out.println(String.format("Response : %d, %s", responseCode, responseMessage));
System.out.println("Response DATA : ");
System.out.println(buffer == null ? "NULL " : buffer.toString());
} catch (IOException e) {
e.printStackTrace();
}
> POST 전송 방식
> data만 사용하면 항상 백그라운드 서비스가 메시지를 수신
> notification을 적용할 경우 Activity가 Foreground 상태일 때와 Background 상태일 때 응답 방식이 변경됨.
> 한글깨짐 문제가 될 수 있으므로 UTF-8 인코딩을 적용.
라벨:
구글 클라우드 메시지,
자바,
FCM
위치:
대한민국
안드로이드 프로젝트 gitignore 리스트
- 프로젝트 복사와 SVN 업로드 시에 불필요한 부분을 덜어내야 관리하기 편리합니다.
1. gitignore 목록
- 프로젝트 루트 디렉토리의 .gitignore 파일은 아래와 같이 설정
*.iml
.gradle/
.idea
/*/build/
/local.properties
/captures
.externalNativeBuild
.DS_Store
Thumbs.db
> app 디렉토리의 .gitignore 파일이 아닌 루트 디렉토리의 .gitignore 파일 입니다.
> 루트 디렉토리의 .gitignore이 없을 경우 새로 생성 하거나 안드로이드 스튜디오에서 직접 gitignore 항목을 지정합니다.
2. 프로젝트 압축 시 용량 줄이기
- 프로젝트 루트 디렉토리
.gradle 디렉토리 제외
.idea 디렉토리 제외
local.properties 파일 제외
.iml 확장자 제외
- 어플리케이션 디렉토리
1. gitignore 목록
- 프로젝트 루트 디렉토리의 .gitignore 파일은 아래와 같이 설정
*.iml
.gradle/
.idea
/*/build/
/local.properties
/captures
.externalNativeBuild
.DS_Store
Thumbs.db
> app 디렉토리의 .gitignore 파일이 아닌 루트 디렉토리의 .gitignore 파일 입니다.
> 루트 디렉토리의 .gitignore이 없을 경우 새로 생성 하거나 안드로이드 스튜디오에서 직접 gitignore 항목을 지정합니다.
2. 프로젝트 압축 시 용량 줄이기
- 프로젝트 루트 디렉토리
.gradle 디렉토리 제외
.idea 디렉토리 제외
local.properties 파일 제외
.iml 확장자 제외
- 어플리케이션 디렉토리
build 디렉토리 제외
.iml 확장자 제외
피드 구독하기:
글 (Atom)