Thursday, November 22, 2018

macOS Visual Studio Code 경로 설정

vi ~/.bash_profile

#VSCode
code () { VSCODE_CWD="$PWD" open -n -b "com.microsoft.VSCode" --args $* ;}

source ~/.bash_profile

Sunday, November 18, 2018

Android 작업관리자에서 표시되지 않게 앱 완전히 종료하기

작업관리자에 앱의 목록이 남지 않으면서 안드로이드 앱을 완벽히 종료하는 방법은 다음과 같다.

moveTaskToBack(true);
if (android.os.Build.VERSION.SDK_INT >= 21) {
    finishAndRemoveTask();
} else {
    finish();
}
android.os.Process.killProcess(android.os.Process.myPid());

기존의 자료에서는 finish()를 사용하라고 되어 있는데 Android 6.0부터는 finishAndRemoveTask()를 호출해야 종료후에 작업관리자의 목록에 앱이 남지 않는다.

Sunday, November 11, 2018

Android Studio 3.1 Kotlin Support 빌드 에러

Android Studio 3.1에서 새로운 프로젝트 생성시 Kotlin Support를 활성화한 후 생성하면 다음과 같이 gradle sync가 에러나는 경우가 있다.

    Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.41.

    Unable to resolve dependency for ':app@debugAndroidTest/compileClasspath': Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.41.

    Unable to resolve dependency for ':app@debugUnitTest/compileClasspath': Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.41.

    Unable to resolve dependency for ':app@release/compileClasspath': Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.41.

    Unable to resolve dependency for ':app@releaseUnitTest/compileClasspath': Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.41.

그럴때는 다음과 같이 app의 build.gradle에서 jre를 jdk로 수정하면 정상적으로 gradle sync 및 빌드가 된다.

    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

Android Google Drive API 예제

안드로이드에서 Google Drive API를 사용하는 예제이다.
구글에서 제공하는 예제넌 quick-start로서 해당 앱의 정보만 업로드하는 기능이고, 사용자의 전체 구글 드라이브의 정보를 리스팅하고 받아오지는 못한다.
우리는 사용자가 기존에 저장해 두었던 정보를 리스팅해 볼것이다.

아래의 페이지에서 사용할 package name과 keystore의 sha-1 키를 등록하자.
https://developers.google.com/drive/android/get-started

그리고 다음과 같이 소스코드를 구현한다. 단 package name은 위에서 등록한 package name이어야 한다.
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "GoogleDriveAndroidAPI";

    private final static int PERMISSION_CONTACTS = 1;
    private static final int REQUEST_AUTHORIZATION = 1;
    private static final int REQUEST_ACCOUNT_PICKER = 2;

    private final HttpTransport m_transport = AndroidHttp.newCompatibleTransport();
    private final JsonFactory m_jsonFactory = GsonFactory.getDefaultInstance();

    private GoogleAccountCredential m_credential;
    private Drive m_client;

    void startGoogleLogin() {
        // Google Accounts using OAuth2
        m_credential = GoogleAccountCredential.usingOAuth2(this, Collections.singleton(DriveScopes.DRIVE));

        m_client = new com.google.api.services.drive.Drive.Builder(
                m_transport, m_jsonFactory, m_credential).setApplicationName("AppName/1.0")
                .build();

        startActivityForResult(m_credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
    }

    boolean checkContactPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // get accounts 권한이 없으면 요청하자
            if (checkSelfPermission(Manifest.permission.GET_ACCOUNTS) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(
                        new String[]{Manifest.permission.GET_ACCOUNTS,

                        },
                        PERMISSION_CONTACTS);
                return false;
            }
        }
        return true;
    }

    public void onRequestContactPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        Log.e("Example", "requestCode=" + requestCode);
        Log.e("Example", "permissions[0]=" + permissions[0]);
        Log.e("Example", "grantResults[0]=" + grantResults[0]);

        if (grantResults[0] == 0) {
            startGoogleLogin();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        onRequestContactPermissionsResult(requestCode, permissions, grantResults);
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (checkContactPermission()) {
            startGoogleLogin();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if ((requestCode == REQUEST_ACCOUNT_PICKER || requestCode == REQUEST_AUTHORIZATION)) {
            if (resultCode == RESULT_OK) {
                if (data != null && data.getExtras() != null) {
                    String accountName = data.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME);
                    if (accountName != null) {
                        m_credential.setSelectedAccountName(accountName);
                    }

                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            String pageToken = null;
                            do {
                                FileList result = null;
                                try {
                                    // GET_ACCOUNTS 권한을  수동으로 제어판 - 애플리케이션에서 설정해주어야 한다.
                                    result = m_client.files().list()
                                            .setQ("'root' in parents")
                                            .setPageSize(20)
                                            .setFields("nextPageToken, files(id, name)")
                                            .execute();
                                    List<File> files = result.getFiles();
                                    if (files == null || files.isEmpty()) {
                                        Log.e(TAG, "No files found.");
                                    } else {
                                        Log.e(TAG, "Files:");
                                        for (File file : files) {
                                            Log.e(TAG, String.format("%s (%s)\n", file.getName(), file.getId()));
                                        }
                                    }
                                    pageToken = result.getNextPageToken();
                                } catch (UserRecoverableAuthIOException e) {
                                    e.printStackTrace();
                                    startActivityForResult(e.getIntent(), REQUEST_AUTHORIZATION);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            } while (pageToken != null);
                        }
                    }).start();
                }
                // call method to start accessing Google Drive
            } else { // CANCELLED
            }
        }
    }
}

Android Thread에서 Dialog 띄우기 (wait, notify 사용)

안드로이드에서 쓰레드 루프 중에 쓰레드를 멈추고 Modal Dialog를 받는 방법을 예제로 만들어 보았다. 동기화에 사용되는 오브젝트는 Object lock으로 wait/notify를 사용하였다.

package com.namjungsoo.www.androidthreadtest;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
    Object lock = new Object();
    void showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        AlertDialog dialog = builder
                .setTitle("title")
                .setMessage("message")
                .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        releaseLock();
                    }
                })
                .setNegativeButton("cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        releaseLock();
                    }
                })
                .setNeutralButton("skip", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        releaseLock();
                    }
                })
                .setCancelable(false)// 백버튼 불가, 바탕화면 클릭 불가
                .create();
        dialog.show();
    }
    void releaseLock() {
        synchronized (lock) {
            lock.notify();
            Log.e("TAG", "notify");
        }
    }
    Runnable runnable;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            showAlert();
                        }
                    });
                    Log.e("TAG", "before wait");
                    synchronized (lock) {
                        lock.wait();
                    }
                    Log.e("TAG", "after wait");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Button btn = (Button) findViewById(R.id.hello);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(runnable).start();
            }
        });
    }
}

Android Studio 3.0 새 프로젝트 빌드 에러 unable to resolve dependency

안드로이드 스튜디오 3.0이 출시되었다. 기존의 프로젝트들은 잘 동작하는데 새로운 프로젝트를 생성하였다면 새로운 프로젝트는 컴파일이 안되는 현상이 발생한다.
Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve com.android.support:appcompat-v7:26.0.0.
Unable to resolve dependency for ':app@debugAndroidTest/compileClasspath': Could not resolve com.android.support.test:runner:1.0.1.
...

이는 gradle plugin 버전이 3.0으로 높아졌고, gradle wrapper 버전이 4.1로 높아진 결과이다. 이 버전업으로 인한 변경점은 다음에 다루겠다.
이 문제를 수정하려면 일단 모듈 app에 대한 build.gradle을 열고 다음과 같이 되어 있는 항목을
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

다음과 같이 변경하면 된다. 변경한 부분은 최신버전이 아닌 부분을 자동으로 최신버전으로 적용하라고 정확한 버전 대신에 ‘+’를 붙여준 것이다.
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.+'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:+'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:+'
}

iOS CocoaPods에서 Admob SDK 7.9.1 버전 사용하기

Admob을 Google SDK페이지에서 시킨대로 사용하다 보면 최신버전은 7.9.1인데 7.8.1이 연동되어 다음과 같은 에러메세지가 나는 경우가 있다.

You are currently using version 7.8.1 of the SDK. Please consider updating your SDK to the most recent SDK version to get the latest features and bug fixes. The latest SDK can be downloaded from http://goo.gl/iGzfsP. A full list of release notes is available at https://developers.google.com/admob/ios/rel-notes.

이렇게 되는 원인은 다음과 같다. Podfile에서 다음과 같이 되어 있는 경우이다.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '7.0'
target 'project' do
  use_frameworks!
  pod 'Firebase'
  pod 'Firebase/Core'
  pod 'Firebase/AdMob'
  pod 'Fabric'
  pod 'Crashlytics'
  pod 'Google/Analytics'
end

이것을 다음과 같이 바꾸자. Firebase/AdMob을 제거하고 수동으로 Google-Mobile-Ads-SDK를 집어 넣자.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '7.0'
target 'nextdoor' do
  use_frameworks!
  pod 'Firebase'
  pod 'Firebase/Core'
#  pod 'Firebase/AdMob'
  pod 'Fabric'
  pod 'Crashlytics'
  pod 'Google/Analytics'
  pod 'Google-Mobile-Ads-SDK'
end

그러면 최신버전 7.9.1의 Admob SDK가 연동된다.
Firebase/AdMob이 구형 7.8.1을 참조해서 문제가 생긴 것이다.

C# Delegate/Event 정리

Delegate/Event
C#은 Java의 interface callback을 쉽게 하기 위한 방법으로 delegate/event를 제공한다.

Delegate (위임, 대리자)
Delegate는 C의 함수포인터와 같은 방식으로 함수를 저장했다가 파라미터로 전달 또는 호출할수 있는 객체이다.

using System;

namespace Delegate
{
    class MainClass
    {
        public delegate void Message(string msg);

        public void Hello(string msg)
        {
            Console.WriteLine("Hello "+msg);  
        }

        public void Run()
        {
            Message message = new Message(Hello);
            message("Delegate!");
        }

        public static void Main(string[] args)
        {
            MainClass mainClass = new MainClass();
            mainClass.Run();
        }
    }
}

C++의 함수 포인터와의 차이점
클래스를 사용하는 C++에는 Pointer to member function이 있는데, 이는 한 클래스의 멤버 함수에 대한 포인터로서 ‘객체’에 대한 컨텍스트를 가지고 있다는 점에서 C#의 delegate와 비슷하다.
단, C#의 delegate는 메서드 Prototype이 같다면 어느 클래스의 메서드도 쉽게 할당할 수 있는데 반해, C++의 Pointer to member는 함수 포인터 선언시 특정 클래스를 지정해주기 때문에 한 클래스에 대해서만 사용할 수 있다.
두번째로 C의 함수 포인터는 하나의 함수 포인터를 갖는데 반해, C# delegate는 하나 이상의 메서드 레퍼런스들을 가질 수 있어서 Multicast가 가능하다.
또한 C의 함수포인터는 Type Safety를 완전히 보장하지 않는 반면, C#의 delegate는 엄격하게 Type Safety를 보장한다.

Event
특별한 형태의 delegate 즉 C# event를 사용할 수 있다. C# event는 할당연산자(=)를 사용할 수 없으며, 오직 이벤트 추가(+= 연산자, Subscribe) 혹은 기존 이벤트 삭제 (-= 연산자, Unsubscribe) 만을 할 수 있다. 또한 delegate 와는 달리 해당 클래스 외부에서는 직접 호출할 수 없다.

using System;

namespace Delegate
{
    class MessageClass
    {
        public delegate void Message(string msg);
        public Message myMessage;
        public event Message myEvent;

        public void Hello(string msg)
        {
            Console.WriteLine("Hello " + msg);
        }

        public void Run()
        {
            Message message = new Message(Hello);
            message("Delegate!");
        }

        public void RunEvent()
        {
            myEvent("RunEvent!");
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            MessageClass mainClass = new MessageClass();
            mainClass.myMessage = mainClass.Hello;
            mainClass.myMessage("External!");

            mainClass.myEvent += mainClass.Hello;

            // event는 클래스 외부에서 실행할수 없음
            //mainClass.myEvent("Event");
            mainClass.RunEvent();
        }
    }
}

Android 8.0 notification channel

안드로이드 8.0에서는 제약사항으로 인해 notification channel을 생성하여야만 notification을 표시할 수가 있다.
아래는 notification channel 생성 예제이다.

class MainActivity : AppCompatActivity() {
    private fun createChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
            val channelMessage = NotificationChannel("channel_id", "channel_name", android.app.NotificationManager.IMPORTANCE_DEFAULT)
            channelMessage.description = "channel description"
            channelMessage.enableLights(true)
            channelMessage.lightColor = Color.GREEN
            channelMessage.enableVibration(true)
            channelMessage.vibrationPattern = longArrayOf(100, 200, 100, 200)
            channelMessage.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
            notificationManager.createNotificationChannel(channelMessage)
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        createChannel()
        val extras = Bundle()
        extras.putString("title", "hello")
        extras.putString("message", "world")
        sendNotification(extras)
    }
    private fun sendNotification(extras: Bundle?) {
        Log.e("MainActivity", "sendNotification")
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val intent = Intent(this, MainActivity::class.java)
        intent.replaceExtras(extras)
        Log.e("MainActivity", "sendNotification intent size=$intent ${intent?.extras}")
        //PendingIntent.FLAG_UPDATE_CURRENT 추가
        //extras 전달을 위해서
        val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        val content = extras?.getString("message")
        val title = extras?.getString("title")
        val builder = NotificationCompat.Builder(this, "channel_id")
        builder.setSmallIcon(R.mipmap.ic_launcher)
                .setContentText(content)
                .setContentTitle(title)
                .setContentIntent(pendingIntent)
        notificationManager.notify(3, builder.build())
    }
}

Thursday, November 8, 2018

Android MediaCodec MediaExtractor TextureView MP4 동영상 플레이하기

Android 내장 미디어 라이브러리인 MediaCodec, MediaExtractor 그리고 렌더링으로는 TextureView를 사용하여 간단히 MP4 동영상을 플레이 해보았다.

public class SurfaceTextureActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
    TextureView mTextureView;
    Surface mSurface;
    private PlayerThread mPlayer = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTextureView = new TextureView(this);
        mTextureView.setSurfaceTextureListener(this);

        setContentView(mTextureView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mPlayer != null) {
            mPlayer.interrupt();
        }
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
        Log.e("SurfaceTextureActivity", "onSurfaceTextureAvailable "+i + " " + i1);
        if (mPlayer == null) {
            mSurface = new Surface(mTextureView.getSurfaceTexture());
            mPlayer = new PlayerThread(mSurface);
            mPlayer.start();
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
        Log.e("SurfaceTextureActivity", "onSurfaceTextureSizeChanged");
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        Log.e("SurfaceTextureActivity", "onSurfaceTextureDestroyed");
        if (mPlayer != null) {
            mPlayer.interrupt();
            return true;
        }
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
        Log.e("SurfaceTextureActivity", "onSurfaceTextureUpdated");

    }
}

public class PlayerThread extends Thread {
    private MediaExtractor extractor;
    private MediaCodec decoder;
    private Surface surface;

    public PlayerThread(Surface surface) {
        this.surface = surface;
    }

    @Override
    public void run() {
        // api 16
        extractor = new MediaExtractor();
        try {
            extractor.setDataSource(SAMPLE);

            for (int i = 0; i < extractor.getTrackCount(); i++) {
                MediaFormat format = extractor.getTrackFormat(i);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("video/")) {
                    extractor.selectTrack(i);
                    decoder = MediaCodec.createDecoderByType(mime);

                    // Test surface disabled
                    decoder.configure(format, surface, null, 0);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (decoder == null) {
            Log.e("DecodeActivity", "Can't find video info!");
            return;
        }

        decoder.start();

        ByteBuffer[] inputBuffers = decoder.getInputBuffers();
        ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
        Log.e("Codec", "inputBuffers.length=" + inputBuffers.length + " outputBuffers.length=" + outputBuffers.length);

        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        boolean isEOS = false;
        long startMs = System.currentTimeMillis();

        //Test
        int frameNo = 0;

        while (!Thread.interrupted()) {
            Log.e("Codec", "presentationTimeUs=" + info.presentationTimeUs);
            if (!isEOS) {
                int inIndex = decoder.dequeueInputBuffer(100000);
                Log.e("Codec", "inIndex=" + inIndex);
                if (inIndex >= 0) {
                    // buffer is available
                    ByteBuffer buffer = inputBuffers[inIndex];

                    // get sample
                    int sampleSize = extractor.readSampleData(buffer, 0);

                    // end of stream
                    if (sampleSize < 0) {
                        // We shouldn't stop the playback at this point, just pass the EOS
                        // flag to decoder, we will get it again from the
                        // dequeueOutputBuffer
                        Log.d("DecodeActivity", "InputBuffer BUFFER_FLAG_END_OF_STREAM");
                        decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        isEOS = true;
                    } else {
                        long sampleTime = extractor.getSampleTime();
                        Log.e("Codec", "sampleTime=" + sampleTime);

                        decoder.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0);

                        long target = sampleTime + 16670;
                        //extractor.seekTo(target, MediaExtractor.SEEK_TO_NEXT_SYNC);
                        extractor.advance();

                        frameNo++;
                        Log.e("Codec", "advance=" + frameNo + " target=" + target);
                    }
                }
            }

            int outIndex = decoder.dequeueOutputBuffer(info, 100000);
            Log.e("Codec", "outIndex=" + outIndex);
            switch (outIndex) {
                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                    Log.d("DecodeActivity", "INFO_OUTPUT_BUFFERS_CHANGED");
                    outputBuffers = decoder.getOutputBuffers();
                    break;
                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                    Log.d("DecodeActivity", "New format " + decoder.getOutputFormat());
                    break;
                case MediaCodec.INFO_TRY_AGAIN_LATER:
                    Log.d("DecodeActivity", "dequeueOutputBuffer timed out!");
                    break;
                default:
                    ByteBuffer buffer = outputBuffers[outIndex];
                    Log.v("DecodeActivity", "We can't use this buffer but render it due to the API limit, " + buffer);

                    // We use a very simple clock to keep the video FPS, or the video
                    // playback will be too fast
                    while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                        try {
                            Log.e("Codec", "sleep 10");
                            sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }

                    decoder.releaseOutputBuffer(outIndex, true);
                    break;
            }

            // All decoded frames have been rendered, we can stop playing now
            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
                Log.e("Codec", "frameNo=" + frameNo);
                break;
            }
        }

        decoder.stop();
        decoder.release();
        extractor.release();
    }
}