结构介绍
续接上集,本期主要给咱的相机进行了一些升级,除了增加2k视频录制功能之外,还要有配套的APP,所以本期博客就是在完成升级任务:
- 视频录制
- ffmpeg解码
- Android13软件开发
视频录制
这里选择了树莓派zero 2W进行视频与图像处理,至于opencv可有可无,opencv录制直接输出mp4文件,而用picamera录制会输出.h264文件,可以通过ffmpeg解码编码为mp4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| import serial from picamera import PiCamera import os import datetime
ser = serial.Serial("/dev/ttyAMA0", 115200) camera = PiCamera()
path = "/var/www/html/" filename_gb = ""
def savePhoto(filename): camera.resolution = (3280, 2460) print("Photo OK") camera.capture(f'{filename}.jpg') return 3
def startVideo(filename): print("Video Start") camera.resolution = (1920, 1080) camera.framerate = 30 camera.video_stabilization = True global filename_gb filename_gb = filename camera.start_recording(f'{filename}.h264') return 8
def stopVideo(): print("Video Stop") global filename_gb camera.stop_recording() cmd = f'ffmpeg -i {filename_gb}.h264 -vcodec copy -acodec copy {filename_gb}.mp4' os.system(cmd) cmd = f'rm {filename_gb}.h264' os.system(cmd) return 4
def readData(): recv_buffer = ser.read(1) return recv_buffer
def main(): data = readData() filename = path + datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") if data == b'1': savePhoto(filename) elif data == b'2': startVideo(filename) elif data == b'0': stopVideo() else: pass ser.flushInput() ser.flushOutput()
if __name__ == "__main__": if os.path.isdir(path): while True: main() pass else: print("Disk Error")
|
UI布局
本来打算在Android内完成的,但是想了一下发现自己对Android的掌握度不高,尤其是kotlin,只能用java写,所以倒不如把相册保存到apache2网站目录后,用php显示,所以下面是显示的php代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
| <?php $path = "./albums"; $imgType = array('png','jpg','mp4'); $show_begin = 0; $step = 20;
$handle = opendir($path); while(($file = readdir($handle)) != false){ list($filename , $kzm) = explode('.', $file); if (in_array($kzm, $imgType)) { if (!is_dir('/'.$file)){ $filelist[] = $file; } } } if(sizeof($filelist) > 0) $total_page = ceil(sizeof($filelist)/$step); else $total_page = 0;
if(isset($_GET["p"])) $page = $_GET["p"]; else $page = 1;
function getFolderSize($dir) { $size = 0; if (is_dir($dir)){ $dh = opendir($dir); while(false !== ($file = @readdir($dh))){ if($file !='.' and $file !='..'){ $path = $dir.'/'.$file; if(is_dir($path)){ $size += dir_size($path); }else if(is_file($path)){ $size += filesize($path); } } } closedir($dh); }else{ echo "路径不是文件夹路径"; } return $size; }
$kb = 1024; $mb = 1024 * 1024; $gb = 1024 * 1024 * 1024; $limitSize = "64GB";
$size = getFolderSize($path); $r = "B";
$pre = $size / ($gb*$limitSize); $pre = round($pre, 2, PHP_ROUND_HALF_UP);
if($size > $kb && $size < $mb){ $size = $size / $kb; $size = round($size, 2, PHP_ROUND_HALF_UP); $r = "KB"; }else if($size > $mb && $size < $gb){ $size = $size / $mb; $size = round($size, 2, PHP_ROUND_HALF_UP); $r = "MB"; }else{ $size = $size / $gb; $size = round($size, 2, PHP_ROUND_HALF_UP); $r = "GB"; } ?>
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Albums</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <link rel="stylesheet" href="mdui/css/mdui.min.css"> <?php if($page<$total_page) echo '<link rel="preload" href="/?p=' .$page+1 . '" as="style" />'; ?> <script>var total = <?php echo $total_page;?>; var pagenow = <?php echo $page;?>;</script> <script src="mdui/js/index.js"></script> <script src="mdui/js/mdui.min.js"></script> </head> <body class="mdui-bottom-nav-fixed"> <div class="mdui-appbar mdui-color-grey-700"> <div class="mdui-toolbar mdui-color-theme"> <a href="https://blog.minloha.cn" class="mdui-typo-title">Micamera</a> <a href="javascript:new mdui.Dialog('#pre_pre').open();" class="mdui-typo-title"><?php echo $size . $r; ?>/<?php echo $limitSize; ?></a> </div> </div> <div class="mdui-container-fluid"> <?php if(sizeof($filelist) != 0) for($i = ($page-1)*$step; $i < $page*$step; $i++){ if($i < sizeof($filelist)){ $f = $filelist[$i]; list($filename , $type_file) = explode('.', $f); echo '<div class="mdui-card">'; echo '<div class="mdui-card-media">'; if($type_file == "jpg") echo '<a href="./albums/'.$f.'"><img src="./albums/'.$f.'" style="width=100%;"/></a>'; else echo '<a href="./albums/'.$f.'"><video><source src="./albums/'.$f.'" type="video/mp4"></video></a>'; echo '<div class="mdui-card-media-covered">'; echo '<div class="mdui-card-primary">'; echo '<div class="mdui-card-primary-title">'.$f.'</div>'; echo '</div>'; echo '</div>'; echo '</div>'; echo '<div class="mdui-card-actions">'; echo '<button class="mdui-btn mdui-ripple" onclick="remove(\'' . $f . '\')">删除</button>'; echo '<a style="text-decoration: none;color: black;" href="./albums/'.$f.'" download="/albums/' . $f . '"><button class="mdui-btn mdui-ripple" >保存</button></a>'; echo '</div>'; echo '</div>'; } } else{ echo '<div class="mdui-dialog" id="dialog">'; echo '<div class="mdui-dialog-title">Micamera提示</div>'; echo '<div class="mdui-dialog-content">相册是空的,没有什么可以展示的呢~</div>'; echo '<div class="mdui-dialog-actions">'; echo '<button class="mdui-btn mdui-ripple">好吧</button>'; echo '</div>'; echo '</div>'; echo "<script>new mdui.Dialog('#dialog').open()</script>"; } ?> <?php if($pre > 0.9){ echo '<div class="mdui-dialog" id="dialog">'; echo '<div class="mdui-dialog-title">Micamera警告</div>'; echo '<div class="mdui-dialog-content">内存空间不足,请删除照片!</div>'; echo '<div class="mdui-dialog-actions">'; echo '<button class="mdui-btn mdui-ripple">收到</button>'; echo '</div>'; echo '</div>'; echo "<script>new mdui.Dialog('#dialog').open();</script>"; } ?> </div> <div class="mdui-bottom-nav mdui-bottom-nav-text-auto mdui-color-grey-700"> <a onclick="sub()" class="mdui-ripple mdui-ripple-white mdui-bottom-nav-active"> <i class="mdui-icon material-icons">keyboard_arrow_left</i> <label>上一页</label> </a> <p>第<?php echo $page; ?> / <?php echo $total_page;?>页</p> <a onclick="add()" class="mdui-ripple mdui-ripple-white mdui-bottom-nav-active"> <i class="mdui-icon material-icons">keyboard_arrow_right</i> <label>下一页</label> </a> </div> <div class="mdui-dialog" id="pre_pre"> <div class="mdui-dialog-title">内存消耗情况:</div> <div class="mdui-p-a-2"> <p>使用空间:<?php echo $size . $r; ?></p> <p>使用占比:</p> </div> <div class="mdui-progress"> <div class="mdui-progress-determinate" style="width: <?php echo round($pre*100,0,PHP_ROUND_HALF_UP);?>%;"></div> </div>
<div class="mdui-dialog-actions"> <button class="mdui-btn mdui-ripple">我看懂了</button> </div> </div> </body> </html>
|
除此之外还要有删除接口,只需要实现一个GET参数的文件删除即可:
1 2 3 4 5 6 7 8
| <?php $filename = $_GET['f']; if(file_exists("./albums/" .$filename)){ unlink("./albums/" . $filename); echo '1'; }else{ echo '0'; }
|
APP部分
这里因为视频文件放在apache2的网页二级目录,所以可以实现一个WebView视图即可,因为WebView无法直接获取返回状态码,所以需要用HttpURLConnection类进行测试连接,如果测试连接成功后WebView再进行连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| package cn.minloha.micamera;
import android.annotation.SuppressLint; import android.app.AlertDialog; import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.net.http.SslError; import android.os.Environment; import android.os.StrictMode; import android.util.Log; import android.view.KeyEvent; import android.webkit.*; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import androidx.core.content.FileProvider;
import java.io.*; import java.net.HttpURLConnection; import java.net.URL;
import static android.os.Environment.DIRECTORY_DCIM; import static android.os.Environment.DIRECTORY_DOWNLOADS;
public class MainActivity extends AppCompatActivity { private WebView browser;
@SuppressLint("SetJavaScriptEnabled") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy);
browser = (WebView)findViewById(R.id.Toweb); browser.getSettings().setDomStorageEnabled(true); browser.getSettings().setDatabaseEnabled(true); browser.getSettings().setJavaScriptEnabled(true); browser.getSettings().setSupportZoom(true); browser.getSettings().setBuiltInZoomControls(true); String path = "http://10.0.4.1/"; try { URL url = new URL(path); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setConnectTimeout(3000); httpURLConnection.connect(); int code = httpURLConnection.getResponseCode(); if (code == 200) browser.loadUrl(path); else{ browser.loadUrl("file:///android_asset/html/non.html"); Log.d("MicameraInfo","Http"); } } catch (IOException e) { browser.loadUrl("file:///android_asset/html/502.html"); Log.d("MicameraInfo","HttpError"); }
browser.setWebViewClient(new WebViewClient() { @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { super.onReceivedHttpError(view, request, errorResponse); int statusCode = errorResponse.getStatusCode(); Log.d("MINFO","Code is:" + statusCode); }
@SuppressLint("WebViewClientOnReceivedSslError") @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { if (handler != null) { handler.proceed(); } } });
browser.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> { String Filetype = url.substring(url.length()-3); String filename = String.valueOf(System.currentTimeMillis()) + url.substring(url.length()-4); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); request.setAllowedOverMetered(true); request.setAllowedOverRoaming(true); String info = ""; if(Filetype.equals("pdf")) { filename = "产品说明书.pdf"; info = "在文件管理器中的文档中查看\"产品说明书.pdf\""; request.setDestinationInExternalPublicDir(DIRECTORY_DOWNLOADS, filename); }else { request.setDestinationInExternalPublicDir(DIRECTORY_DCIM, filename); info = "在相册中的DCIM下查看"; } final DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); long downloadId = downloadManager.enqueue(request); new AlertDialog.Builder(this).setTitle("下载成功!").setMessage(info).setPositiveButton("我知道了",null).show(); });
}
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { WebView browser=(WebView)findViewById(R.id.Toweb); if ((keyCode == KeyEvent.KEYCODE_BACK) && browser.canGoBack()) { browser.goBack(); return true; } return super.onKeyDown(keyCode, event); }
}
|
总结
没啥难度,在外壳上花费的时间比较多,软件很快就可以调通的。目前还差面板没有3D打印,剩下就是打胶和组装了!