本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
直播封面上传功能运行效果
直播界面讲解
直播标题
当前直播的标题,内容简介
直播封面
封面图片会在如上图显示以及直播列表上显示
是否录制
直播结束之后需要观看直播,就需要录制,直播结束之后录制的视频文件就叫点播,可以对视频进行永久的存储
摄像头直播
根据主播需求打开前置摄像头还是后置摄像头
录屏直播
只要用于屏幕录播,例如:游戏 在5.0以上
录制清晰度
码流和分辨率(流畅,超清,高清)决定
界面布局不贴了,读者自己看着界面做,这里需要提示一下,界面上用到的自定义控件在文章中有讲,请戳链接。
【从 0 开始开发一款直播 APP】13 Android 6.0 运行时权限
【从 0 开始开发一款直播 APP】14 animation-list 逐帧动画自定义Switch控件
直播封面上传
直播封面上传功能有以下几个:
运行时权限验证
上传本地图片
上传相机图片
1、运行时权限验证
PublishPresenter # checkPublishPermission()
//1、权限通过 ActivityCompat 类的 checkSelfPermission() 方法判断是否有所需权限。
//2、权限请求是通过 ActivityCompat 类中的 requestPermissions() 方法,在OnRequestPermissionsResultCallback # onRequestPermissionsResult() 方法中回调。
//----------------------------split line---------------------------------------
@Override
public boolean checkPublishPermission(Activity activity) {
if (Build.VERSION.SDK_INT >= 23) {
List<String> permissions = new ArrayList<>();
//写入外部存储设备权限(保存封面图片)
if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
//相机权限
if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)) {
permissions.add(Manifest.permission.CAMERA);
}
//ActivityCompat.requestPermissions() 请求权限
if (permissions.size() != 0) {
ActivityCompat.requestPermissions(activity
, permissions.toArray(new String[0]),
Constants.WRITE_PERMISSION_REQ_CODE);
return false;
}
}
return true;
}
PublishActivity # onRequestPermissionsResult()
onRequestPermissionsResult() 处理请求权限
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
//写入外部存储权限
case Constants.WRITE_PERMISSION_REQ_CODE:
for (int ret : grantResults) {
if (ret != PackageManager.PERMISSION_GRANTED) {
return;
}
}
mPermission = true;
break;
}
}
2、上传图片(相机、本地)
依然采用 MVP 架构。
【从 0 开始开发一款直播 APP】5.1 MVP 完全解析 -- 实现直播登录
View — IPublishView
public interface IPublishView extends BaseView{
Activity getActivity();
/**
* 定位成功
* @param location 位置
*/
void doLocationSuccess(String location);
/**
* 定位失败
*/
void doLocationFailed();
/**
* 图片上传成功
* @param url 路径
*/
void doUploadSuccess(String url);
/**
* 图片上传失败
* @param url 路径
*/
void doUploadFailed(String url);
/**
* 结束页面
*/
void finishActivity();
}
Presenter — IPublishPresenter
public abstract class IPublishPresenter implements BasePresenter {
protected BaseView mBaseView;
public IPublishPresenter(BaseView baseView) {
mBaseView = baseView;
}
/**
* 检查推流权限
* @param activity
* @return
*/
public abstract boolean checkPublishPermission(Activity activity);
/**
* 裁剪图片
* @param imgUri 图片地址
* @return
*/
public abstract Uri cropImage(Uri imgUri);
/**
* 选择图片方式:相机、相册
* @param mPermission 权限
* @param type 类型
* @return
*/
public abstract Uri pickImage(boolean mPermission,int type);
/**
* 上传图片
* @param path 图片路径
*/
public abstract void doUploadPic(String path);
}
图片上传请求实体类
图片要上传到服务端,就需要请求网络,对图片上传封装一个请求实体。
public class UploadPicRequest extends IRequest {
//http://live.demo.cniao5.com/Api/Image/upload
//请求参数:userId type file
public UploadPicRequest(int requestId, String userId, int type, File file) throws FileNotFoundException {
mRequestId = requestId;
mParams.put("userId",userId);
mParams.put("type",type);
mParams.put("file",file);
}
@Override
public String getUrl() {
return getHost() + "Image/upload";
}
@Override
public Type getParserType() {
return new TypeToken<Response<UploadResp>>() {}.getType();
}
}
PublishPresenter 具体实现
图片裁剪请查看:详细解释如何通过Android自带的方式来实现图片的裁剪——原理分析+解决方案
public class PublishPresenter extends IPublishPresenter {
private IPublishView mIPublishView;
private boolean mUploading = false;
private String TAG = PublishPresenter.class.getSimpleName();
public PublishPresenter(IPublishView iPublishView) {
super(iPublishView);
this.mIPublishView = iPublishView;
}
@Override
public void start() {
}
@Override
public void finish() {
mIPublishView.finishActivity();
}
/**
* 直接调用系统的图片裁剪功能
* @param uri
* @return
*/
@Override
public Uri cropImage(Uri uri) {
Uri cropUri = createCoverUri("_crop");
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");//可以选择图片类型,如果是 * 表明所有类型的图片
intent.putExtra("crop", "true");//设置在开启的Intent中设置显示的 view 可裁剪
intent.putExtra("aspectX", 750);//裁剪图片的比例
intent.putExtra("aspectY", 550);
intent.putExtra("outputX", 750);//裁剪图片的宽
intent.putExtra("outputY", 550);
intent.putExtra("scale", true);//是否保持比例
intent.putExtra("return-data", false);//是否返回bitmap
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);//保存图片到指定uri
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());//输出格式
mIPublishView.getActivity().startActivityForResult(intent, Constants.CROP_CHOOSE);//启动裁剪功能
return cropUri;
}
//存储封面图片并保存uri地址
private Uri createCoverUri(String preFileName) {
String filename = ImUserInfoMgr.getInstance().getUserId() + preFileName + ".jpg";
String path = Environment.getExternalStorageDirectory() + "/cniao_live";
File outputImage = new File(path, filename);
if (ContextCompat.checkSelfPermission(mIPublishView.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(mIPublishView.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Constants.WRITE_PERMISSION_REQ_CODE);
return null;
}
try {
File pathFile = new File(path);
if (!pathFile.exists())
pathFile.mkdirs();
if (outputImage.exists())
outputImage.delete();
} catch (Exception e) {
e.printStackTrace();
mIPublishView.showMsg("生成封面失败");
}
return Uri.fromFile(outputImage);
}
//选择封面图来源(相机、相册)
@Override
public Uri pickImage(boolean mPermission, int type) {
Uri fileUri = null;
if (!mPermission) {
mIPublishView.showMsg("权限不足");
return null;
}
switch (type) {
//相机
case Constants.PICK_IMAGE_CAMERA:
fileUri = createCoverUri("");
//启动相机
Intent intent_photo = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent_photo.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
mIPublishView.getActivity().startActivityForResult(intent_photo, Constants.PICK_IMAGE_CAMERA);
break;
//本地相册
case Constants.PICK_IMAGE_LOCAL:
fileUri = createCoverUri("_select");
//打开相册
Intent intent_album = new Intent("android.intent.action.GET_CONTENT");
intent_album.setType("image/*");
mIPublishView.getActivity().startActivityForResult(intent_album, Constants.PICK_IMAGE_LOCAL);
break;
}
return fileUri;
}
//上传封面图
@Override
public void doUploadPic(String path) {
mUploading = true;
try {
final UploadPicRequest request = new UploadPicRequest(1000,
ACache.get(mIPublishView.getContext()).getAsString("user_id"),
Constants.LIVE_COVER_TYPE,new File(path));
AsyncHttp.instance().post(request, new AsyncHttp.IHttpListener() {
@Override
public void onStart(int requestId) {
}
@Override
public void onSuccess(int requestId, Response response) {
if (response!=null) {
UploadResp resp = (UploadResp) response.getData();
Log.i(TAG, "onSuccess url:" + resp.getUrl());
mIPublishView.doUploadSuccess(resp.getUrl());
}else {
Log.i(TAG, "onSuccess url:");
}
}
@Override
public void onFailure(int requestId, int httpStatus, Throwable error) {
Log.i(TAG, "onFailure :" + error);
mIPublishView.doLocationFailed();
}
});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
封面图片上传实现
整个大致流程:首先检测权限,权限添加之后点击封面图片调用封面选择对话框,会出现三个按钮(相机、相册、取消)。
相机:打开相机功能进行拍照,然后确定之后会调用系统裁剪功能对图片进行裁剪,接着会将图片进行保存,裁剪之后再点击确定就会调用上传图片功能。
相册:打开相册选择,选择之后会调用系统裁剪功能对图片进行裁剪,接着会将图片进行保存,裁剪之后再点击确定就会调用上传图片功能。
取消:点击取消则退出当前对话框。
public class PublishActivity extends BaseActivity implements View.OnClickListener, IPublishView{
//图片封面文字
private TextView mTvPicTip;
private Dialog mPicDialog;//选择封面对话框(照相机、相册、取消)
private ImageView mImgCover;//封面图
private Uri mFileUri, mCropUri;//原始图片文件uri,裁剪之后的图片文件uri
private boolean mPermission = false;//权限监测
private PublishPresenter mPublishPresenter;
private String TAG = PublishActivity.class.getSimpleName();
@Override
protected void setActionBar() {
}
@Override
protected void setListener() {
mImgCover.setOnClickListener(this);
}
@Override
protected void initData() {
//初始化PublishPresenter
mPublishPresenter = new PublishPresenter(this);
//检测权限
mPermission = mPublishPresenter.checkPublishPermission(this);
String strCover = ACache.get(this).getAsString("head_pic");
if (!TextUtils.isEmpty(strCover)) {
Log.e(TAG, "head_pic:" + strCover);
Glide.with(this).load(strCover).into(mImgCover);
mTvPicTip.setVisibility(View.GONE);
} else {
mImgCover.setImageResource(R.drawable.publish_background);
}
}
@Override
protected void initView() {
mTvPicTip = obtainView(R.id.tv_pic_tip);
mImgCover = obtainView(R.id.cover);
//初始化图片选择对话框
initPhotoDialog();
}
/**
* 封面图片选择对话框
*/
private void initPhotoDialog() {
//对话框初始化及样式设置
mPicDialog = new Dialog(this, R.style.float_dialog);
//对话框布局
mPicDialog.setContentView(R.layout.dialog_pic_choose);
WindowManager windowManager = getWindowManager();
Display display = windowManager.getDefaultDisplay();
Window window = mPicDialog.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
window.setGravity(Gravity.BOTTOM);
lp.width = display.getWidth();
mPicDialog.getWindow().setAttributes(lp);
//按钮初始化并添加点击事件
mPicDialog.findViewById(R.id.tv_chose_camera).setOnClickListener(this);
mPicDialog.findViewById(R.id.tv_pic_lib).setOnClickListener(this);
mPicDialog.findViewById(R.id.tv_dialog_cancel).setOnClickListener(this);
}
@Override
protected int getLayoutId() {
return R.layout.activity_publish;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
//封面图片选择
case R.id.cover:
mPicDialog.show();
break;
//相机,对拍摄图片地址进行存储,并对图片进行裁剪
case R.id.tv_chose_camera:
mFileUri = mPublishPresenter.pickImage(mPermission, Constants.PICK_IMAGE_CAMERA);
mPicDialog.dismiss();
break;
//相册,从本地相册选择图片作为封面,并对图片进行裁剪,并将地址进行保存
case R.id.tv_pic_lib:
mFileUri = mPublishPresenter.pickImage(mPermission, Constants.PICK_IMAGE_LOCAL);
mPicDialog.dismiss();
break;
//取消对话框按钮,表示不添加封面图片
case R.id.tv_dialog_cancel:
mPicDialog.dismiss();
break;
}
}
//相机和相册选择结果回调
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch (requestCode) {
//选择相机图片
case Constants.PICK_IMAGE_CAMERA:
mCropUri = mPublishPresenter.cropImage(mFileUri);
Log.d(TAG, "cropImage->path camera:" + mCropUri.getPath());
Log.d(TAG,"PICK_IMAGE_CAMERA 选择相机图片成功");
break;
//选择本地相册图片
case Constants.PICK_IMAGE_LOCAL:
String path = OtherUtils.getPath(this, data.getData());
if (null != path) {
Log.d(TAG, "cropImage->path local:" + path);
File file = new File(path);
mCropUri = mPublishPresenter.cropImage(Uri.fromFile(file));
}
Log.e(TAG,"PICK_IMAGE_LOCAL 选择本地图片成功");
break;
//上传相机/相册图片
case Constants.CROP_CHOOSE:
mTvPicTip.setVisibility(View.GONE);
Log.d(TAG, "cropImage->path crop:" + mCropUri.getPath());
mPublishPresenter.doUploadPic(mCropUri.getPath());
Log.d(TAG,"CROP_CHOOSE 上传图片成功");
break;
}
}
}
@Override
public void doUploadSuccess(String url) {
//加载封面图
Glide.with(this).load(url).into(mImgCover);
}
@Override
public void doUploadFailed(String url) {
showMsg("直播封面上传失败");
}
//其他某些实现方法已被删,代码量太大,只贴出主要代码
//......
}
运行效果
在控制台可以查看到打印的 Log 信息,先选择图片,然后裁剪之后进行上传。上传到服务端会返回一个 url 地址。打开 url 可以在浏览器中查看到图片。
在手机上找到 cniao_live 文件夹,并且有刚刚上传过的图片。
详情请转至 GitHub