نمایش و پخش ویدئو امروزه به یکی از پرکاربردترین قابلیتهای گوشیهای هوشمند تبدیل شده. چه ویدئوهای آفلاین موجود روی کارت حافظه موبایل و چه استریم ویدئوهای آنلاین از طریق اینترنت. در این جلسه از سری مطالب آموزش برنامه نویسی اندروید به نحوه پخش ویدئو در اندروید توسط VideoView میپردازیم.
معرفی VideoView اندروید
به نام خدا. در اندروید برای نمایش یک ویدئو از VideoView استفاده میشود. این کامپوننت از فرمتهای رایج MP4 (H.263 و H.264) و ۳GP پشتیبانی میکند. با استفاده از VideoView پخش ویدئو در اندروید از طریق منابع مختلف شامل فایلهای منابع برنامه (پوشه raw زیرمجموعه پوشه res پروژه)، فایلهای روی کارت حافظه دیوایس (local) و همچنین ویدئوهای آنلاین (URL) امکان پذیر است.
ابتدا متدهای VideoView را به صورت مختصر توضیح داده و در ادامه تعدادی از آنها را در قالب یک پروژه اندرویدی بکار میبریم.
۱: setVideoUri() و setVideoPath(): از این دو متد برای تعیین آدرس ویدئو(ها) استفاده میشود.
۲: pause(): این متد ویدئوی در حال پخش را در حالت مکث (توقف موقت) قرار میدهد.
۳: canPause(): این متد بررسی میکند آیا ویدئوی در حال پخش میتواند pause شود یا نه. خروجی این متد true یا false است. چنانچه مقدار true برگرداند یعنی امکان مکث وجود دارد در غیر اینصورت مکث ممکن نیست.
۴: seekTo(int millisecond): توسط این متد میتوان پخش ویدئو را از موقعیت مشخصی (برحسب میلی ثانیه) آغاز کرد.
۵: resume(): با فراخوانی این متد، ویدئو از نقطهای که قبلا pause شده پخش میشود.
۶: stopPlayback(): این متد دستور توقف ویدئو را صادر میکند.
۷: canSeekForward(): بررسی میکند آیا ویدئو امکان پرش به جلو را دارد یا نه. خروجی true یا false خواهد بود.
۸: canSeekBackward(): بررسی میکند آیا ویدئو امکان برگشت به عقب را دارد یا نه. خروجی true یا false خواهد بود.
۹: getDuration(): مدت زمان ویدئو را برمیگرداند.
۱۰: isPlaying(): بررسی میکند آیا ویدئو در حال پخش است یا نه. خروجی true یا false خواهد بود.
۱۱: setOnPreparedListener(MediaPlayer.OnPreparedListener): هنگامی که ویدئو آماده پخش شد این متد در صورتی که تعریف شده باشد فراخوانی خواهد شد. این متد کاربردهای فراوانی دارد و عملیاتهایی که هنگام پخش ویدئو باید انجام شوند را میتوانیم درون این متد تعریف کنیم.
۱۲: setOnErrorListener(MediaPlayer.OnErrorListener): چنانچه در پخش ویدئو خطایی رخ دهد این متد فراخوانی میشود. بنابراین اقدامات لازم برای بعد از دریافت خطا (مانند اطلاع به کاربر، بستن صفحه و…) را درون این متد تعریف میکنیم.
۱۳: setOnCompletionListener(MediaPlayer.OnCompletionListener): بعد از پایان ویدئو چنانچه این متد تعریف شده باشد فراخوانی خواهد شد. کارهایی که لازم است بعد از پایان ویدئو انجام شود (مانند خروج از صفحه، پیشنهاد ویدئوهای دیگر و…) درون این متد انجام خواهد شد.
کنترل ویدئو توسط MediaController
VideoView صرفا وظیفه پخش ویدئو را به عهده دارد و هیچ ابزار کنترلی را به کاربر ارائه نمیدهد. یعنی مدیریت ویدئو شامل عقب یا جلو بردن ویدئو، توقف و شروع پخش از عهده این کامپوننت خارج است. برای مدیریت ویدئوی در حال پخش از MediaController استفاده میکنیم.
در تصویر فوق یک ابزار کنترل ویدئو مشاهده میکنید. با اتصال یک MediaController به VideoView این ابزار به طور خودکار به صفحهای که ویدئو در حال پخش است اضافه میشود.
پروژه پخش ویدئو در اندروید توسط VideoView
در این قسمت و در قالب یک پروژه ساده، کار با VideoView و تعدادی از متدهای آن و همچنین کار با MediaController را تمرین میکنیم.
مطابق مبحث آموزش ساخت پروژه در اندروید استودیو یک پروژه اندرویدی با نام VideoVIew میسازم. اکتیویتی را از نوع Empty Activity و زبان را Java انتخاب کردم.
ابتدا یک ویجت VideoView به Layout اکتیویتی اضافه میکنم:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <VideoView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/video_view" /> </LinearLayout>
گفتیم برای پخش ویدئو در اندروید از منابع آنلاین و آفلاین استفاده میشود. منابع آفلاین شامل فایلهای داخل برنامه (resources) و همچنین فایلهای روی حافظه دیوایس اندرویدی هستند.
قبلا در مطالبی مانند آموزش پخش صوت در اندروید توسط MediaPlayer به نحوه ساخت پوشه raw در پروژه اندروید و اضافه کردن فایل به آن پرداختهایم. یک ویدئو با پسوند MP4 به raw پروژه اضافه میکنم:
حالا اکتیویتی را به صورت زیر تکمیل میکنم:
MainActivity.java
package ir.android_studio.videoview; import androidx.appcompat.app.AppCompatActivity; import android.net.Uri; import android.os.Bundle; import android.widget.VideoView; public class MainActivity extends AppCompatActivity { VideoView myVideoView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myVideoView = findViewById(R.id.video_view); Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.patmat); myVideoView.setVideoURI(videoUri); myVideoView.start(); } }
ابتدا یک نمونه از VideoView با نام myVideoView تعریف کردم. برای اعلام محل قرارگیری فایل ویدئو به VideoView از متد setVideoURI() استفاده میکنیم. بنابراین یک نمونه از URI با نام videoURI تعریف کرده و فایل ویدئوی مدنظرم را تعیین میکنم:
Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.patmat);
URI (مخفف Uniform Resource Identifier) آدرس یک فایل را اعلام میکند. درست مانند URL در وب که برای تعیین آدرس صفحه وب بکار میرود. getPackageName() نام پکیج برنامه را برمیگرداند یعنی “ir.android_studio.videoview”. در خط بعد متد setVideoURI با ورودی videoUri تعریف شده. در نهایت ویدئو توسط متد start() اجرا و پخش میشود.
پروژه را اجرا میکنم:
ویدئو در حال پخش است اما همانطور که ابتدای جلسه اشاره کردم هیچ کنترلی روی ویدئو نداریم. یک نمونه از کلاس MediaController با نام دلخواه mController میسازم. سپس توسط متد setMediaController باید myVideoView را به کنترولر متصل کنم:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myVideoView = findViewById(R.id.video_view); MediaController mController = new MediaController(this); myVideoView.setMediaController(mController); Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.patmat); myVideoView.setVideoURI(videoUri); myVideoView.start(); }
دوباره پروژه را اجرا میکنم:
با کلیک روی ویدئوی در حال پخش، نوار کنترولر ظاهر میشود که شامل دکمههای عقب، جلو، توقف و seekBar ای که در دو طرف آن موقعیت فعلی ویدئو و مدت زمان آن است. اما هنوز یک مشکل دیگر داریم. این کنترولر باید روی ویدئو (قسمت پایین آن) نمایش داده شود در صورتی که در تصویر بالا این دو از هم فاصله دارند. برای اینکار از setAnchorView() استفاده میکنیم. این متد بر اساس وضعیت قرارگیری VideoView و طول و عرض آن، کنترولر را در قسمت پایینی VideoView تنظیم میکند.
برای اینکه به وضعیت و محل قرارگیری ویدئو و طول و عرض آن دسترسی داشته باشیم باید از متد setOnPreparedListener() استفاده کنیم. لغت Prepared یعنی “آماده شده”. بنابراین از نحوه نامگذاری این متد مشخص میشود که یک شنونده برای زمانی است که VideoView در وضعیت آماده قرار گرفته و موقعیت آن مشخص شده است.
متد setOnPreparedListener را به اکتیویتی اضافه میکنم:
به اینصورت تکمیل میشود:
myVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { } });
درون این متد یک متد دیگر با نام setOnVideoSizeChangedListener تعریف میکنم. این متد هنگامی که اندازه و موقعیت ویدئو تعیین میشود اجرا خواهد شد:
متد به اینصورت تکمیل شد:
myVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mediaPlayer, int i, int i1) { } }); } });
در نهایت متد setAnchorView() را داخل این متد تعریف میکنم:
myVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mediaPlayer, int i, int i1) { mController.setAnchorView(myVideoView); } }); } });
کد کامل اکتیویتی:
MainActivity.java
package ir.android_studio.videoview; import androidx.appcompat.app.AppCompatActivity; import android.media.MediaPlayer; import android.widget.MediaController; import android.net.Uri; import android.os.Bundle; import android.widget.VideoView; public class MainActivity extends AppCompatActivity { VideoView myVideoView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myVideoView = findViewById(R.id.video_view); final MediaController mController = new MediaController(this); myVideoView.setMediaController(mController); Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.patmat); myVideoView.setVideoURI(videoUri); myVideoView.start(); myVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mediaPlayer, int i, int i1) { mController.setAnchorView(myVideoView); } }); } }); } }
پروژه را اجرا میکنم:
مشاهده میکنید اینبار کنترولر روی خود ویدئو قرار دارد.
MediaController متدهای دیگری هم دارد:
۱: show(): این متد کنترولر را به نمایش در میآورد. یعنی در صورتی که این متد فراخوانی شود نیازی نیست روی ویدئو کلیک شود و به صورت پیش فرض کنترولر فعال خواهد بود:
mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mediaPlayer, int i, int i1) { mController.setAnchorView(myVideoView); mController.show(); } });
۲: show(int timeout): عملکرد این متد مانند مورد قبل است با این تفاوت که میتوان مدت زمان نمایش کنترولر را در واحد میلی ثانیه تعیین کرد:
mController.show(3000);
به عنوان مثال کد فوق، کنترولر را به مدت ۳ ثانیه نمایش داده و سپس مخفی میکند. پس از مخفی شدن در صورتی که روی ویدئو کلیک شود، مجدد کنترولر ظاهر و پس از ۳ ثانیه مخفی خواهد شد.
۳: hide(): برخلاف دو متد قبل، این متد کنترولر را مخفی میکند.
۴: isShowing(): این متد بررسی میکند آیا کنترولر در حال نمایش است یا خیر و بر اساس آن کاری را انجام دهد. به عنوان مثال متد show() را درون این شرط قرار میدهم تا فقط در صورتی اجرا شود که isShowing() مقدار false برگردانده باشد:
if (mController.isShowing()) { // Do something } else { mController.show(3000); }
فعلا به MediaController کاری ندارم. در ادامه سایر متدهای VideoView را بررسی میکنم.
متد setOnPreparedListener زمانی اجرا میشد که ویدئو آماده پخش شده بود. متد دیگری با نام setOnCompletionListener داریم که در هنگام به پایان رسیدن ویدئو و توقف آن اجرا میشود. کلمه Completion به معنی “تکمیل” است که کاربرد متد را میرساند. برای مثال با این متد میتوانیم تعیین کنیم بعد از پایان ویدئو، صفحه بسته شود و یا هر عمل دیگری که لازم داریم. من توسط یک Toast کارکرد متد را تست میکنم:
myVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { Toast.makeText(MainActivity.this, "ویدئو به پایان رسید", Toast.LENGTH_SHORT).show(); } });
با اجرای پروژه و پس از پایان پخش ویدئو، پیغام نمایش داده میشود:
یک Listener دیگر هم برای هنگام دریافت ارور در پخش ویدئو استفاده میشود که setOnErrorListener نام دارد:
myVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mediaPlayer, int i, int i1) { Toast.makeText(MainActivity.this, "در پخش ویدئو اشکالی وجود دارد", Toast.LENGTH_SHORT).show(); return false; } });
در مبحث چرخه حیات اکتیویتی در اندروید با انواع متدهای اکتیویتی آشنا شدیم. هنگامی که جهت صفحه نمایش از افقی (landscape) به عمودی (portrate) و یا بلعکس تغییر پیدا کند و یا اکتیویتی به حالت background رفته و دوباره روی صفحه نمایش ظاهر شود، در این فرآیند اکتیویتی متوقف شده و دوباره ساخته میشود. VideoView امکان ذخیره کردن وضعیت خود را ندارد بنابراین چنانچه یکی از این دو حالت در حین پخش ویدئو اتفاق بیفتد، موقعیت فعلی ویدئو ذخیره نخواهد شد. یعنی اگر در حین پخش، کاربر جهت قرارگیری دیوایس اندرویدی خود را تغییر داده و یا از اکتیویتی خارج شده و مجدد به آن بازگردد، ویدئو از ابتدا پخش خواهد شد و نه از جایی که قبلا متوقف شده.
برای حل این مسئله از متدهای onSaveInstanceState() و onRestoreInstanceState() کلاس اکتیویتی استفاده میکنیم. متد onSaveInstanceState برای ذخیره (save) داده هنگام متوقف شدن اکتیویتی و onRestoreInstanceState برای بازیابی (restore) دادهها هنگام ساخت مجدد اکتیویتی فراخوانی میشوند.
داخل کلاس اکتیویتی و بعد از متد onCreate() دو متد فوق را اضافه کرده و به صورت زیر تکمیل میکنم:
@Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("MyVideoPosition", myVideoView.getCurrentPosition()); myVideoView.pause(); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); videoPosition = savedInstanceState.getInt("MyVideoPosition"); myVideoView.seekTo(videoPosition); }
ابتدا باید در متد onSaveInstanceState آخرین موقعیت ویدئوی در حال پخش را ذخیره کنم. اینکار را با استفاده از متد putInt() انجام میدهم. این متد دو ورودی دارد. ورودی اول یک کلید هست که هنگام بازیابی دادهها گرفته میشود. من نام دلخواه MyVideoPosition را تعیین کردم. ورودی دوم یک int است که توسط متد getCurrentPosition کامپوننت VideoView موقعیت فعلی ویدئو را میگیرم. موقعیت بر حسب واحد میلی ثانیه ذخیره میشود. سپس ویدئو توسط متد pause() متوقف شده است.
در ادامه درون کلاس یک متغیر از جنس int و مقدار اولیه ۰ تعریف میکنم:
private int videoPosition = 0;
حالا درون onRestoreInstanceState مقداری که با کلید MyVideoPosition توسط putInt ذخیره شده بود را اینجا و به وسیله getInt دریافت کرده و در videoPosition ذخیره میکنم. سپس توسط متد دیگری از VideoView به نام seekTo() موقعیت ذخیره شده (به واحد میلی ثانیه) را به VideoView اعلام میکنم تا ویدئو از این نقطه پخش شود.
پروژه را اجرا میکنم:
در تصاویر بالا مشاهده میکنید در حین پخش ویدئو دوبار جهت قرارگیری صفحه نمایش تغییر کرده با اینحال پخش ویدئو از نقطه قبلی ادامه پیدا میکند و نه از ابتدا.
نکته:
همانطور که اشاره شد دو متد
onSaveInstanceState()
و
onRestoreInstanceState()
در دو حالت کاربرد داشته و اجرا میشوند؛ هنگام چرخش صفحه نمایش و یا متوقف شدن اکتیویتی و ساخته شدن مجدد آن. اما چنانچه بخواهیم اکتیویتی فقط موقع چرخش صفحه restart نشود بدون نیاز به این دو متد و صرفا با تعریف یک ویژگی (attribute) در تگ activity مربوطه در مانیفست پروژه این کار امکان پذیر است:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ir.android_studio.videoview"> <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"> <activity android:name=".MainActivity" android:configChanges="orientation"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
در تگ activity مربوط به MainActivity یک ویژگی با نام configChanges و مقدار orientation تعریف کردم. این ویژگی بر دو متد تعریف شده در کلاس اکتیویتی مقدم بوده و چنانچه configChanges با مقدار orientatios و متدهای save و restore داده به طور همزمان در اکتیویتی تعریف شده باشند، هنگام چرخش صفحه توسط ویژگی فوق از ریستارت شدن اکتیویتی جلوگیری خواهد شد.
نکته: حجم نهایی اپلیکیشن در تصمیم کاربران برای نصب یا عدم نصب آن نقش بسزایی دارد. بنابراین اضافه کردن فایلهای ویدئویی به پروژه گزینه مطلوبی نبوده و بهتر است برنامه ما ویدئوها را از طریق اینترنت دریافت و پخش کند.
پخش ویدئو آنلاین توسط VideoView در اندروید
برای پخش ویدئو در اندروید به صورت آنلاین، ابتدا باید مجوز دسترسی به اینترنت را به برنامه بدهیم. قبلا در مطلب آموزش کتابخانه Retrofit با بحث مجوزها آشنا شدیم. مجوز INTERNET را به مانیفست اضافه میکنم:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ir.android_studio.videoview"> <uses-permission android:name="android.permission.INTERNET" /> <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"> <activity android:name=".MainActivity" android:configChanges="orientation"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
حالا کافیست بجای آدرس محلی فایل، آدرس فایل روی سرور را توسط یکی از متدهای setVideoURI یا setVideoPath به VideoView پاس دهیم.
setVideoURI:
Uri videoUri = Uri.parse("http://dl.android-studio.ir/files/patmat.mp4"); myVideoView.setVideoURI(videoUri);
setVideoPath:
myVideoView.setVideoPath("http://dl.android-studio.ir/files/patmat.mp4");
کد نهایی اکتیویتی:
MainActivity.java
package ir.android_studio.videoview; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.media.MediaPlayer; import android.os.PersistableBundle; import android.util.Log; import android.widget.MediaController; import android.net.Uri; import android.os.Bundle; import android.widget.Toast; import android.widget.VideoView; public class MainActivity extends AppCompatActivity { VideoView myVideoView; private int videoPosition = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); myVideoView = findViewById(R.id.video_view); final MediaController mController = new MediaController(this); myVideoView.setMediaController(mController); //Uri videoUri = Uri.parse("http://dl.android-studio.ir/files/patmat.mp4"); //myVideoView.setVideoPath("http://dl.android-studio.ir/files/patmat.mp4"); Uri videoUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.patmat); myVideoView.setVideoURI(videoUri); myVideoView.start(); myVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mediaPlayer, int i, int i1) { mController.setAnchorView(myVideoView); if (mController.isShowing()) { // Do something } else { mController.show(3000); } } }); } }); myVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { Toast.makeText(MainActivity.this, "ویدئو به پایان رسید", Toast.LENGTH_SHORT).show(); } }); myVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mediaPlayer, int i, int i1) { Toast.makeText(MainActivity.this, "اشکالی در پخش ویدئو وجود دارد", Toast.LENGTH_SHORT).show(); return false; } }); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("MyVideoPosition", myVideoView.getCurrentPosition()); myVideoView.pause(); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); videoPosition = savedInstanceState.getInt("MyVideoPosition"); myVideoView.seekTo(videoPosition); } }
موفق و پیروز باشید.
مطالعهی بیشتر:
https://developer.android.com/reference/android/widget/VideoView
https://developer.android.com/reference/android/widget/MediaControllerhttps://developer.android.com/reference/android/app/Activityhttps://developer.android.com/guide/topics/resources/runtime-changeshttps://developer.android.com/guide/topics/manifest/activity-element
توجه : سورس پروژه درون پوشه Exercises قرار دارد