/***
* 将文件切割成片
* @param filename
* @param uuid
* @param data
* @throws IOException
*/
default void divideToSegments(String filename, String uuid, byte[]data) throws IOException { DivideTask divideTask = new DivideTask(filename,uuid,data); Future<ImmutablePair<PlayList, List<TransportSegment>>> divideFuture = getThreadPool().submit(divideTask); String mediaId = String.format("media.%s",uuid); try {
ImmutablePair<PlayList, List<TransportSegment>> plsAndTsFiles = divideFuture.get(30, TimeUnit.MINUTES);
PlayList playlist = plsAndTsFiles.getLeft();
List<TransportSegment> segments = plsAndTsFiles.getRight(); //保存切片文件
saveSegments(segments);
//保存播放列表
savePlayList(playlist); //放到缓存里
Map<String,String> mapping = new HashMap<>();
mapping.put("playlist",playlist.getContext());
//把原始文件放进去,方便以后下载
mapping.put("binary",Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(filename)))); for (TransportSegment segment:segments)
{
String tsFileName = segment.getFilename();
byte[] bytes = segment.getBytes();
String binary = Base64.getEncoder().encodeToString(bytes);
mapping.put(tsFileName,binary);
} //切片以后的文件添加到缓存
getCacheService().setCacheMap(mediaId, mapping); //30分钟以后失效
getCacheService().expire(mediaId,7,TimeUnit.DAYS); } catch (InterruptedException| ExecutionException | TimeoutException e) {
getLogger().error("文件切片失败:{}",e);
}
}
import lombok.Data;
import lombok.NoArgsConstructor; import javax.persistence.Table;
import java.time.LocalDateTime;
import java.time.ZoneId; /***
* 转换后的文件切片
*/
@Data
@NoArgsConstructor
@Table(name = "open_segment")
public class TransportSegment
{
String uuid; /***
* 文件名
*/
private String filename; /***
* 字节流
*/
private byte[] bytes; private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); private TransportSegment(Builder builder) {
setUuid(builder.uuid);
setFilename(builder.filename);
setBytes(builder.bytes);
setCreateTime(builder.createTime);
} public static final class Builder {
private String uuid;
private String filename;
private byte[] bytes;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder filename(String filename) {
this.filename = filename;
return this;
} public Builder bytes(byte[] bytes) {
this.bytes = bytes;
return this;
} public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
} public TransportSegment build() {
return new TransportSegment(this);
}
}
}
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime;
import java.time.ZoneId; @NoArgsConstructor
@Data
public class PlayList {
private String uuid;
/***
* 播放时长
*/
private Float duration; /***
* 播放列表内容
*/
private String context; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); private PlayList(Builder builder) {
setUuid(builder.uuid);
setDuration(builder.duration);
setContext(builder.context);
setCreateTime(builder.createTime);
} public static final class Builder {
private String uuid;
private Float duration;
private String context;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder duration(Float duration) {
this.duration = duration;
return this;
} public Builder context(String context) {
this.context = context;
return this;
} public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
} public PlayList build() {
return new PlayList(this);
}
}
}
    /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
String hashKey = entry.getKey();
if(hashKey !=null){
hashOperations.put(key, hashKey, entry.getValue());
}
else {
log.error("出错了:{},hash键为null@{}",entry.getValue());
}
}
}
return hashOperations;
}
    @Override
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
import lombok.Data;
import lombok.NoArgsConstructor;
import net.bramp.ffmpeg.probe.FFmpegStream;
import org.apache.ibatis.type.JdbcType;
import tk.mybatis.mapper.annotation.ColumnType; import javax.persistence.Table;
import java.util.List; @Data
@NoArgsConstructor
@Table(name = "open_media")
public class AudioMediaFile extends MediaFile { /***
* 流通道数
*/
@ColumnType(column = "nb_streams",jdbcType = JdbcType.TINYINT)
byte nbStreams; byte nbPrograms; Integer startTime;
/***
* 格式名称
*/
String formatName; /***
* 多媒体播放时长
*/
Float duration; /***
* 比特率
*/
Integer bitRate; @ColumnType(column = "probe_score",jdbcType = JdbcType.TINYINT)
byte probeScore; /***
* 文件类型
*/
@ColumnType(column = "type",jdbcType = JdbcType.TINYINT)
byte type; List<FFmpegStream> streams; String metadata; private AudioMediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
setNbStreams(builder.nbStreams);
setFormatName(builder.formatName);
setDuration(builder.duration);
setBitRate(builder.bitRate);
setProbeScore(builder.probeScore);
setType(builder.type);
setStreams(builder.streams);
setMetadata(builder.metadata);
} public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size;
private byte nbStreams;
private String formatName;
private Float duration;
private Integer bitRate;
private byte probeScore;
private byte type;
private List<FFmpegStream> streams;
private String metadata; public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder name(String name) {
this.name = name;
return this;
} public Builder data(byte[] data) {
this.data = data;
return this;
} public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
} public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
} public Builder size(Long size) {
this.size = size;
return this;
} public Builder nbStreams(byte nbStreams) {
this.nbStreams = nbStreams;
return this;
} public Builder formatName(String formatName) {
this.formatName = formatName;
return this;
} public Builder duration(Float duration) {
this.duration = duration;
return this;
} public Builder bitRate(Integer bitRate) {
this.bitRate = bitRate;
return this;
} public Builder probeScore(byte probeScore) {
this.probeScore = probeScore;
return this;
} public Builder type(byte type) {
this.type = type;
return this;
} public Builder streams(List<FFmpegStream> streams) {
this.streams = streams;
return this;
} public Builder metadata(String metadata) {
this.metadata = metadata;
return this;
} public AudioMediaFile build() {
return new AudioMediaFile(this);
}
}
}
import lombok.Data;
import lombok.NoArgsConstructor; import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table; /***
* 多媒体文件
*/
@Data
@NoArgsConstructor
@Table(name = "open_media")
public class MediaFile { /**
* 音频文件
*/
public static final byte TYPE_AUDIO = 0x1; public static final byte TYPE_VIDEO = 0x2; public static final byte TYPE_DATA1 = 0x4; public static final byte TYPE_DATA2 = 0x8; /***
* 文件唯一标识
*/
@Id
@GeneratedValue(generator = "JDBC")
String uuid; /****
* 文件名
*/
String name; /***
* 解析后的数据流
*/
byte[] data;
/***
* 多媒体文件类型
*/
String mimeType; /***
* 创建文件的时间
*/
Long stamp; /***
* 文件大小
*/
Long size; private MediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
} public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size; public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder name(String name) {
this.name = name;
return this;
} public Builder data(byte[] data) {
this.data = data;
return this;
} public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
} public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
} public Builder size(Long size) {
this.size = size;
return this;
} public MediaFile build() {
return new MediaFile(this);
}
}
}
    static String toJson(Object value) throws JsonProcessingException {
ObjectMapper objectMapper =new ObjectMapper();
//属性值为null不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//默认值的不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
//反斜杠转义其他字符
objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER,true);
//所有键值用字符串形式包装起来
objectMapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS,true);
return objectMapper.writeValueAsString(value);
}
import xxx.bean.AudioMediaFile;
import xxx.bean.MediaFile;
import xxx.bean.PlayList;
import xxx.bean.TransportSegment;
import xxx.service.MediaService;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFmpegUtils;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.job.FFmpegJob;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.probe.FFmpegStream;
import net.bramp.ffmpeg.progress.Progress;
import net.bramp.ffmpeg.progress.ProgressListener;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; /***
* 文件切割线程任务
* divides it into a series of small media segments of equal duration.
* @author dqk
*/
@Deprecated
@Slf4j
public class DivideTask implements Callable<ImmutablePair<PlayList, List<TransportSegment>>>
{ final Locale locale = Locale.US;
final FFmpeg ffmpeg = new FFmpeg();
final FFprobe ffprobe = new FFprobe(); ImmutablePair<FFmpegProbeResult, AudioMediaFile> pair; String filename;
String uuid;
byte[] data; public DivideTask(ImmutablePair<FFmpegProbeResult,AudioMediaFile> pair) throws IOException {
this.pair = pair;
} public DivideTask(String filename,String uuid,byte[] data) throws IOException {
this.filename = filename;
this.uuid = uuid;
this.data = data; //获取反序列化后文件的元数据信息
FFmpegProbeResult probeResult = ffprobe.probe(filename); long timestamp = LocalDateTime.now(ZoneId.of("UTC+8")).toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); String metadata = MediaService.toJson(probeResult); AudioMediaFile.Builder builder = new AudioMediaFile.Builder()
.name(filename)
.uuid(uuid)
.streams(probeResult.streams)
.mimeType(probeResult.format.format_long_name)
.type(MediaFile.TYPE_AUDIO)
.stamp(timestamp)
.bitRate(Long.valueOf(probeResult.format.bit_rate).intValue())
.duration(Double.valueOf(probeResult.format.duration).floatValue())
.formatName(probeResult.format.format_name)
.nbStreams((byte) probeResult.format.nb_streams)
.size(probeResult.format.size)
.probeScore((byte) probeResult.format.probe_score)
.mimeType(probeResult.format.format_long_name)
.data(data)
.metadata(metadata);
this.pair = new ImmutablePair<>(probeResult,builder.build());
} public static String getString(InputStream stream) throws IOException {
return IOUtils.toString(stream,"UTF-8");
} @Override
public ImmutablePair<PlayList,List<TransportSegment>> call() throws Exception { FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe); final FFmpegProbeResult probe = pair.getLeft();
AudioMediaFile audioFile = pair.getRight(); final List<FFmpegStream> streams = probe.getStreams().stream().filter(fFmpegStream -> fFmpegStream.codec_type!=null).collect(Collectors.toList()); final Optional<FFmpegStream> audioStream = streams.stream().filter(fFmpegStream -> FFmpegStream.CodecType.AUDIO.equals(fFmpegStream.codec_type)).findFirst(); if(!audioStream.isPresent())
{
log.error("未发现音频流");
} String filename = probe.format.filename; Path nioFile = Paths.get(filename); String directory = nioFile.getParent().toString(); String uuid = audioFile.getUuid(); String output = String.format("%s%sstream.m3u8",directory, File.separator); FFmpegBuilder builder = new FFmpegBuilder()
.setInput(filename)
.overrideOutputFiles(true)
.addOutput(output)
.setFormat("wav")
.setAudioBitRate(audioStream.isPresent()?audioStream.get().bit_rate:0)
.setAudioChannels(1)
.setAudioCodec("aac") // using the aac codec
.setAudioSampleRate(audioStream.get().sample_rate)
.setAudioBitRate(audioStream.get().bit_rate)
.setStrict(FFmpegBuilder.Strict.STRICT)
.setFormat("hls")
.addExtraArgs("-hls_wrap", "0", "-hls_time", "5", "-hls_list_size","0")
.done(); FFmpegJob job =
executor.createJob(
builder,
new ProgressListener() { // Using the FFmpegProbeResult determine the duration of the input
final double duration_ns = probe.getFormat().duration * TimeUnit.SECONDS.toNanos(1); @Override
public void progress(Progress progress) {
double percentage = progress.out_time_ns / duration_ns; // Print out interesting information about the progress
String consoleLog = String.format(
locale,
"[%.0f%%] status:%s frame:%d time:%s fps:%.0f speed:%.2fx",
percentage * 100,
progress.status,
progress.frame,
FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
progress.fps.doubleValue(),
progress.speed);
log.debug(consoleLog);
}
}); job.run(); if (job.getState() == FFmpegJob.State.FINISHED) { //排除的文件
String[] excludes = new String[]{
"wav","m3u8"
}; List<TransportSegment> segments = Files.list(Paths.get(directory)).filter(
path -> {
String extension = getFileExtension(path.getFileName().toString());
return !Arrays.asList(excludes).contains(extension);
}
).map(path -> {
String name = path.getFileName().toString();
try {
byte[] bytes = IOUtils.toByteArray(path.toUri());
TransportSegment segment = new TransportSegment
.Builder()
.bytes(bytes)
.filename(name)
.uuid(uuid)
.build();
return segment;
} catch (IOException e) {
log.error("读取文件失败:{}",e);
}
return null;
}).collect(Collectors.toList()); String context = getString(new FileInputStream(output));
PlayList playList = new PlayList.Builder()
.context(context)
.uuid(uuid)
.duration(Double.valueOf(probe.format.duration).floatValue())
.build(); return new ImmutablePair<>(playList,segments); }else {
log.error("文件分割发生不可预料的错误:{}");
} return null;
} private static String getFileExtension(String fileName) {
if (fileName.lastIndexOf(".") != -1 && fileName.lastIndexOf(".") != 0) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
} else {
return "";
}
} }

最终生成结果

前端代码:

        var ctlVolume =$("#volume");
//音量
var level = ctlVolume.attr("min")/ctlVolume.attr("max"); var player = videojs('example-video');
// player.ready(function() {
// var _this = this
// //速率
// var playbackRate = $("#playbackRate").val();
// var speed = parseFloat(playbackRate);
//
// var volume = parseFloat($("#volume").val()/100.0);
//
// setTimeout(function() {
// _this.playbackRate(speed);
// _this.volume(volume);
// },20);
// }); var data = response.data;
var message = '消息:'+response.message+",code:"+response.code+",meta:"+JSON.stringify(data);
console.info(message); player.src('/media/'+data.uuid+'.m3u8');
player.play();
@RequestMapping(value = "{uuid}.m3u8")
public ResponseEntity<StreamingResponseBody> m3u8Generator(@PathVariable("uuid") String uuid){ String key = "media.".concat(uuid);
Map<String, Object> cached = cacheService.getCacheMap(key);
if(CollectionUtils.isEmpty(cached))
{
return new ResponseEntity(null, HttpStatus.OK);
}
String playlist = (String) cached.get("playlist");
String[] lines = playlist.split("\n"); //人为在每个MPEG-2 transport stream文件前面加上一个地址前缀
StringBuffer buffer = new StringBuffer(); StreamingResponseBody responseBody = new StreamingResponseBody() {
@Override
public void writeTo (OutputStream out) throws IOException {
for(int i = 0; i < lines.length; i++)
{
String line = lines[i]; if(line.endsWith(".ts"))
{
buffer.append("/streaming/");
buffer.append(uuid);
buffer.append("/");
buffer.append(line);
}else {
buffer.append(line);
}
buffer.append("\r\n");
}
out.write(buffer.toString().getBytes());
out.flush();
}
}; return new ResponseEntity(responseBody, HttpStatus.OK);
}

2020-1-7日更新

方法补充:getCacheService().setCacheMap(mediaId, mapping);

    /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
<T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap); /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
hashOperations.putAll(key, dataMap);
}
return hashOperations;
}

saveSegments方法

  <insert id="saveSegments">
INSERT INTO open_segment(
uuid
,filename
,bytes
,create_time
)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.uuid}
,#{item.filename}
,#{item.bytes}
,#{item.createTime}
)
</foreach>
</insert>

savePlayList方法

<insert id="savePlayList" useGeneratedKeys="true" keyProperty="id">
insert into open_playlist(uuid, duration, context, create_time)
values (#{uuid}, #{duration}, #{context}, #{createTime})
</insert>

java使用ffmpeg生成HLS切片文件的更多相关文章

  1. Java程序如何生成Jar 执行文件(2)

    一.用Eclipse生产Jar文件 注意:此方法可以打包含有第三方jar包的项目 1. 首先,右键你的Java工程,选择Export,在Java文件夹下选择Runnable JAR file,如下图所 ...

  2. Java程序如何生成Jar 执行文件(1)

    一.用Eclipse生产Jar文件 注意:此方法只能打包简单程序,不包含含有第三方jar包的项目 首先,看一下我的项目的目录结构: 1,项目名字上面点右键,选择Export,在选择java\JAR f ...

  3. java使用jdom生成xml格式文件

    本文生成xml使用的工具是jdom.jar,下载地址如下: 链接:https://eyun.baidu.com/s/3slyHgnj 密码:0TXF 生成之后的文档格式类型,就如上面的图片一样,简单吧 ...

  4. 转:如何利用已有的切片文件生成TPK

    Tpk是ArcGIS 10.1即将推出的一种新的数据文件类型,主要是用于将切片文件打包形成离线地图包.Tpk可以在ArcGIS Runtime中作为切片底图被加载.在ArcGIS 10.1中Tpk的生 ...

  5. 使用ffmpeg搭建HLS直播系统

    [时间:2018-04] [状态:Open] [关键词:流媒体,stream,HLS, ffmpeg,live,直播,点播, nginx, ssegment] 0 引言 本文作为HLS综述的后续文章. ...

  6. Java使用FFmpeg处理视频文件指南

    Java使用FFmpeg处理视频文件指南 本文主要讲述如何使用Java + FFmpeg实现对视频文件的信息提取.码率压缩.分辨率转换等功能: 之前在网上浏览了一大圈Java使用FFmpeg处理音视频 ...

  7. 10.nginx+ffmpeg上搭建HLS切片

    1.首先介绍一下HLS协议: (1)简介 这个协议是由苹果公司提出并推广使用的,维基百科介绍如下: HTTP Live Streaming(缩写是HLS)是一个由苹果公司提出的基于HTTP的流媒体网络 ...

  8. Java使用FFmpeg处理视频文件的方法教程

    这篇文章主要给大家介绍了关于Java使用FFmpeg处理视频文件的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 前言 本文主要 ...

  9. java中如何生成可执行的jar文件

    java中如何生成可执行的jar文件 最简单的方法就是: jar -cfe Card.jar CardLayoutDemo CardLayoutDemo$1.class CardLayoutDemo$ ...

随机推荐

  1. js正则验证input输入框有空格时提示直接去除空格

    <input type="text" id="test"/> <input type="button" value=&qu ...

  2. Ubuntu18.04下安装Sublime Text3并解决不能输入中文

    Ubuntu18.04下安装Sublime Text3并解决不能输入中文! 废话不多说,直接按顺序执行下面命令开始安装! wget -qO - https://download.sublimetext ...

  3. [c++11]右值引用、移动语义和完美转发

    c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的 ...

  4. Hdu 2157 How many ways??(DP||矩阵乘法)

    How many ways?? Time Limit:1000 MS Memory Limit: 32768 K Problem Description 春天到了, HDU校园里开满了花, 姹紫嫣红, ...

  5. 自主设计BootLoader框架笔记一栏

  6. 建立自己的键盘栈(shortcutkeyStack)

    建立自己的键盘栈(shortcutkeyStack) 作为一名开发者, 快捷键是必不可少的, 并且各种开发工具都有提供快捷键. 但是各种工具(IDE,编辑器)因为历史或者其他不可抗原因(比如键盘的布局 ...

  7. 《挑战30天C++入门极限》入门教程:C++中的const限定修饰符

        入门教程:C++中的const限定修饰符 const修饰符可以把对象转变成常数对象,什么意思呢? 意思就是说利用const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用! ...

  8. golang-package fmt

    package fmt import "fmt" mt包实现了类似C语言printf和scanf的格式化I/O.格式化动作('verb')源自C语言但更简单. Printing v ...

  9. 割点 —— Tarjan 算法

    由于对于这一块掌握的十分不好,所以在昨天做题的过程中一直困扰着我,好不容易搞懂了,写个小总结吧 qwq~ 割点 概念 在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点 ...

  10. CSP-S2019 快乐爆0

    hhh 我爆0了 快乐 大家都比我强 hh 常规操作 本来就是个憨憨 回去复习文化课了 唉 干啥啥不行